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. 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:
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).
Same question as (1), but let's say we want to make it Optional<...> as well.
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.
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");
}
...
}
}