Accepting (optional) string inputs of various formats

I recently decided to try to do snazzy (well, for me at least) things with traits, because they seem scary and I’m all about jumping into the deep end.

I immediately sank to the bottom.

I have a method that can create an object that has an optional short option and an optional long option name:

impl<C> Spec<C> {
  pub fn new_opt<S: Into<String>>(sopt: Option<char>, lopt: Option<S>,
        proc: Handler<C>) -> Self {
    if sopt.is_none() && lopt.is_none() {
      panic!("Options require short or long name");
    }

    // ...

    Spec { sopt, lopt: new_lopt, proc: Callback::Opt(proc),
        ..Default::default() }
  }
}

I assume the problem is obvious, but it wasn’t to me. :slight_smile: This works fine as long as one passes a character or None to sopt, but if None is passed to lopt it breaks the Into constraint.

Now, I’ve seen multiple traits be “added together” using ‘+’ as a separator. Is this what I should be looking for? I tried to see if there was some kind of “OptionalTrait” thing I could use, but couldn’t find any.

More broadly:

  1. What’s the preferred way to allow any arbitrary string as an input to an function (i.e. noting is really known about the caller; Into seemed very nice in this regard).
  2. Same question as (1), but let’s say we want to make it Optional<…> as well.

The reason why you cannot pass None as argument for lopt is that Rust doesn’t know which None you mean:

  • do you mean Option::<String>::None?
  • do you mean Option::<&'static str>::None?
  • do you mean Option::<SomeOtherType>::None?

Depending on your answer, Rust has to call different functions. In the first case, you call Spec<SomeC>::new_opt::<String>(), etc.

Unfortunately, I don’t have a good idea how to solve this issue. But maybe just knowing why this happens gives you an idea how to move forward. :slightly_smiling_face:

1 Like

Note that you can write None::<String> to make it less wordy.

2 Likes

A common technique is the Builder pattern. This pattern actually has a number of uses:

  • Simulating named arguments (to prevent multiple arguments with the same type from being accidentally swapped by the caller)
  • Future-proofing APIs by allowing new arguments to be added backwards compatibly.
  • …and of course, as a neater alternative to Option<S: SomeTrait>.

What is the pattern? Basically, you take a function (or group of functions that take related arguments) and turn it into a type with the following:

  • a new method that creates a default configuration. (if it doesn’t take any arguments, then it should also be accompanied by an impl of the Default trait that does the same)
  • a bunch of trivial setter methods representing the “arguments” to your function. (as a rule of thumb these shouldn’t do any validation!)
  • one or more “build” methods; these represent the actual, original function(s).
    • It doesn’t have to actually be called build (in fact I rarely see this name used in practice); that’s just a common naming convention when the thing you’re writing is a constructor.
    • Sometimes it makes sense for build to take an argument, e.g. if there’s one that’s required like your proc argument.

Some examples:

  • std::fs::OpenOptions. (the “build” method is named open)
  • std::process::Command. (this has multiple “build” methods: spawn, output, status)
  • clap::Arg. (obviously closely related to what you’re doing! Also, this one has multiple new functions: with_name and from_usage)

In this particular instance it helps because users don’t have to write None; if they don’t have a long option, then they simply don’t have to call the lopt setter!

// Here I'm deriving Default because all my args are Option and
// None makes a sensible default for all of them.
#[derive(Default)]
pub struct Builder {
    sopt: Option<char>,
    lopt: Option<String>,
}

impl Builder {
    pub fn new() -> Self { Default::default() }

    pub fn sopt(mut self, c: char) -> Self {
        self.sopt = Some(c);
        self
    }

    pub fn lopt<S: Into<String>>(mut self, string: S) -> Self {
        self.lopt = Some(string.into());
        self
    }

    pub fn build<C>(self, proc: Handler<C>) -> Spec<C> {
        if sopt.is_none() && lopt.is_none() {
            panic!("Options require short or long name");
        }
        ...
    }
}
4 Likes