Cfgs and traits in signatures

It seems like just yesterday, has it been three months I have been working on this? I should have asked for help back when I made this post.

I made a library.

Kind of a wrapper / framework. The basic premise is to turn a function into a command line program. The API is designed to be super easy to use. Write a function that takes one struct as an argument and returns a Result. The struct must be a clap derive struct for the program's command line options. If the function does not have any input/command options, then just make an empty clap stuct, but the struct must have one special option, the library control options.

[derive(clap::Parser, Debug)]
[clap(name = "program", about = "a program from a function.")]
struct Opt {
    // The special nomain_controls item must exist in your struct
    // You MUST include the following two lines.
    #[clap(flatten)]
    nomain_controls: nomain::control::Controls,
}

Thanks to generics the function can return a Result of any type.

program.rs

fn program(opt: &mut Opt) -> Result<i32,i32>{
    println!("{:?}", opt);
    Ok(0)
}

The library will parse the command line arguments with clap, then pass a reference to the function and process the Result. At the most basic, the wrapper will return 0 or 1 to the OS depending on the Result Ok, or Err.

That works. Great. I figured out how to clap and pass functions and process errors. Very good.

Inside the library at this simple state there is a function that looks something like this. (Note the signature. U, V)

pub fn control<T: ControlOptions, U, V>(
    program: fn(&mut T) -> Result<U, V>,
    mut controls: T,
) -> i32 {
    // logic ...
    // logic ...
    match program(&mut controls) {
        Ok(_ok) => {
            // do Ok stuff
            return 0;
        }
        Err(_err) => {
            // do Err stuff
            return 1;
        }
    }
}

So it works. I can write a function, pass it to my library through a macro, and the library wraps it in a main, calls clap and executes the function. Cool.

Now let me mention I have read all the books, from "The Book" to "nomicon" to "too many lists"..
Reading and comprehending are not the same....

So I planned a simple method to add more magic. I will keep my library simple. Linear. Easy to understand and parse the code. My features will just "add". They shall not conflict unless necessary. To use the library should remain the same regardless of what features are used.

I have a struct, the ControlOptions that will hold all of the libraries command line options.
These are what you see when you use the --help option in your program (in addition to whatever options your function takes). Of course because my library is designed to be simple, if you don't use a feature, you don't get those options in your --help and they do not exist in the executable. The struct looks something like this.

#[derive(clap::Parser, Debug, Default)]
pub struct Controls {
    /// Dry run mode
    ///
    /// .   pass    : Pass dry run responsibility to the program.
    ///
    /// .   control : The controller logic runs but does not call the program.
    ///
    /// .   noop    : Do nothing.
    #[clap(long = "nomain-dry-run", arg_enum)]
    #[cfg(feature = "dry_run")]
    dry_run: Option<DryRun>,

    ///  Verbosity level
    ///
    /// .   silent  : Be quiet.  Only show fatal messages.
    ///
    /// .   error   : Show error messages.
    ///
    /// .   warning : Show warning messages.
    ///
    /// .   info    : Show normal information.
    ///
    /// .   chatter : Show too much information.
    #[clap(long = "nomain-verbosity", arg_enum, default_value = "warn")]
    #[cfg(feature = "verbose")]
    verbosity: Verbosity,

    /// Debug
    ///
    /// Add extra debug output.
    #[clap(long = "nomain-debug")]
    #[cfg(feature = "debug")]
    debug: bool,

    /// Number of times to execute program
    #[clap(long = "nomain-loop-times", default_value_t = 1)]
    #[cfg(feature = "loop_times")]
    loop_times: usize,
    #[clap(skip)]
    #[cfg(feature = "loop")]
    loop_count: usize,
 // more stuff
}

I don't know if this "wrong" use of features, but it appears very simple to understand. And it works.

Inside my control() function I added some looping. It works. I added some loop logic that checks if Ok or Err and repeats or breaks out and exits. It works. I added some "verbosity". All of these additions are very linear, simple to understand, I think. Something like this.

//  Before my loop I have a simple test to see if zero was passed
//  as a command line option.  If we execute the function 0 times just return
    #[cfg(feature = "loop_times")]
    if controls.loop_times() == 0 {
        return 0;
    }

// inside my loop we have some verbose stuff.
        #[cfg(feature = "verbose")]
        controls.chatter(format!("Loop ({})", controls.loop_count()).as_ref());

// inside my loop I have various logic that checks if we break.
                #[cfg(feature = "loop_until")]
                if controls.loop_until_ok() {
                    break;
                }

So everything is in my mind, easy to understand and easy to add more magic as I go. Then I get to a feature, part of my original design. I wanted to get the framework done first. So now build what I want. This feature will "look" at the Ok and compare it to a String. If the thing inside the Ok matches the String, then break out, execution done, if it does not match continue looping. Simple. But that means the thing inside the Ok, must have the Display trait to compare the String. I know a little bit about traits. I know there are bounds, but I think the where clause is very linear and would work nice with my method of adding magic.

pub fn control<T: ControlOptions, U, V,> (
where
    #[cfg(feature = "ok_display")]
    U: std::fmt::Display,
{

Doh....

error: expected one of `{`, lifetime, or type, found `#`
  |
  | where
  |      - expected one of `{`, lifetime, or type
  |     #[cfg(feature = "ok_display")]
  |     ^ unexpected token

Darn, that does not work. So I played a bit and found this neat trick.

I replaced

pub fn control<T: ControlOptions, U, V,> (

with


pub fn control<T: ControlOptions,
    #[cfg(not(feature = "ok_display"))] U,
    #[cfg(feature = "ok_display")] U: std::fmt::Display,
    V,> (

A little "wordy" but it does work. Bonus points if you see the big problem...

So great now if a use wants to use the "ok_display" feature they need to implement the Display trait for whatever they return in the Ok and if they do not use that feature, they don't need to implement Display for their Ok thing. That works. I test and I can loop 100 times or until my
random generator returns Ok(42). Cool So I got that, a bit wordy but I can handle it. Let me proceed to add another feature. To keep the example simple, let us say we want to see the Debug of the thing in the Ok. Following same method I want to of course check if the user does not want Display and only wants Debug or both, or neither. So the function signature starts to explode And if I were to add a couple more features that require the U to have a trait, for example Hash
and Serde.....The code gets huge very fast. Big O(too much) factorial?
(possible typos below)

pub fn control<T: ControlOptions,
    #[cfg(not(any(feature = "ok_display", feature = "ok_debug")))] U,
    #[cfg(all(feature = "ok_display", feature = "ok_debug"))] U: std::fmt::Display + std::fmt::Debug,
    #[cfg(all(feature = "ok_display",not(feature = "ok_debug")))] U: std::fmt::Display,
    #[cfg(all(feature = "ok_debug",not(feature = "ok_display")))] U: std::fmt::Debug,
    V,> (

So my library works, and I am now at a point that I want to factor a little bit. But putting all of that nonsense in any more function signatures is a sure NO. If you have been able to understand me so far...
Is there a way I can keep that signature simple. Maybe I need more understanding of traits and can make traits for ProgOk, and ProgErr and just write

pub fn control<T: ControlOptions, U: ProgOK, V: ProgErr>

and then have all the cfgs put somewhere else where I impl the ProgOk and ProgErr.

Any helpful hints?

Looks like you can do that.

trait ProgOk {}

impl<
    #[cfg(not(any(feature = "ok_display", feature = "ok_debug")))] U,
    #[cfg(all(feature = "ok_display", feature = "ok_debug"))] U: std::fmt::Display + std::fmt::Debug,
    #[cfg(all(feature = "ok_display",not(feature = "ok_debug")))] U: std::fmt::Display,
    #[cfg(all(feature = "ok_debug",not(feature = "ok_display")))] U: std::fmt::Debug,
> ProgOk for U {}

Each unrelated feature can be on or off, so that's 2n combinations.

Thanks.
I have this mental block. I had to write the whole question, format it, reference my code, previous posts, etc, before I imagined the workaround as I stated in the last sentence.

Now I will actually go do the typing...if it works, I will move on to the "saveable" feature. One of the main features, reasons for this library. Save the Ok's in files or append them into one big file.

In the future, before the ProgOk impl gets huge. (I have a total of 4 planned features that will affect that impl with the cfgs, but then as I was posting this question I thought up Hash would be a good thing to compare in addition to Display/Debug and I have no idea what else I will think of.) In the future I guess I might figure out how to generate that part without the cfg's. Maybe with the "build script" thing I read about.

Walked and ate and hacked and ugg... my brain is confused and this is totally wrong. I don't want my library user to have to implement ProgOk. Just Display or Debug.. I have more thinking to do....

Not uncommon. :duck:

I thought of a way to make this linear instead.

  • Have a trait per feature
  • If the feature is enabled, implement for types that implement the required trait(s)
  • Else, implement for all types
  • Have your overall trait depend on all feature traits as supertraits
  • implement for all types that meet the bounds

(Sorry for not providing an example - on mobile - can do so later if the above is not clear.)

1 Like

I will play around a bit. Thanks

Example.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.