Ergonomic optional parameters: impl<T> From<T> for Option<T>?

I think this would be a great ergonomics win for functions that take optional parameters. Eg, for a function:

fn myfn(a: Option<u8>, b: Option<u8>, c: Option<u8>) -> u8;

Right now you must call it as

myfn(Some(0), None, Some(2))

or similar. With impl From<T> for Option<T>, you’d be able to call it as

myfn(0, None, 2)

which removes a lot of noise at the call site. The definition of myfn would gain some noise:

fn myfn(t: T, u: U, v: V) -> Option
    where T: Into<Option<u8>>,
          U: Into<Option<u8>>,
          V: Into<Option<u8>>;

However, I think that pushing the noise into the definition makes for nicer user code overall.

I feel like this is an obvious impl, and so there must be a good reason it’s not already in std. Am I missing something? Should I just make a PR from this commit?

1 Like
fn f<T, U>(v: U) where U: Into<Option<T>> { ...}

f::<i32, _>(42); // Ok.  Wraps.
f::<i32, _>(Some(42)); // Still ok.  Doesn't wrap.
f::<Option<i32>, _>(Some(42)); // Suddenly wraps!

I really don’t like “magic” like this that causes code to suddenly change meaning depending on context.

Could you give a more concrete example? Having difficulty figuring out what this would look like in real code.

That’s difficult when you’re asking me to demonstrate the real-world problems with a non-existent feature. :stuck_out_tongue:

My point is that this would mean what x does depends on what the inferred type is; in some cases it would pass through directly, in others it would wrap in a Some. I mean, if you say None in the source, did you mean no value, or Some(None)?

Thankfully, it wouldn’t be as bad as null or Python-esque None, but it still seems a needless complication.

I think that, if you want optional arguments, they should be a language feature.

(Now, implementing Extend on Option, that makes perfect sense…)

In real world, there isnt much use of having Option as a function argument, because you usually only really care about the Some case. If you only care about the Some case, dont use Option, and just use T!

If you want optional arguments, I dont think shoehorning them in this way is a good idea.

So how would you do optional arguments?

The various IO types have set_{read,write}_timeout methods that take Option<Duration> and invocations of them could be cleaned up by this kind of change.

1 Like

My impression of std::convert::From and std::convert::Into is that they change the “representation” of things but not the “meaning” of things, where to me, T and Option<T> are quite different in meaning.

I agree with @WiSaGaN

Time to revive removed operators such as ~ or @ ? :slightly_smiling:

Can you think of a problem with this feature that does not rely on the (highly) unrealistic type Option<Option<T>>? It seems silly to abandon a feature which only malfunctions in this particular corner case.

It can be patched by adding a meta-trait Definite which is possessed by every type except Option (I’m not sure if the language allows this) and then writing impl From<T> for Option<T> where T: Definite. This covers approximately every situation the original proposal does, because optional options are pretty rare.

I don’t know if I agree with Option<Option<T>> to be unrealistic. E g, a message handler might give back None if the message was not handled at all, Some(None) if the message was handled but did not require a reply, or Some(Some(Reply)) if there was a reply.

We usually need to set a default value when the argument is None. In my opionion, a better approach is to introduce a trait like:

#![feature(specialization)]

trait Argument<T> {
    fn value(self) -> T;
}

impl<T> Argument<T> for T {
    default fn value(self) -> T { self }
}

impl<T: Default> Argument<T> for Option<T> {
    fn value(self) -> T { self.unwrap_or_default() }
}

fn example<T, U, V, W>(t: T, u: U, v: V, w: W) -> i32
    where T: Argument<i32>,
          U: Argument<i32>,
          V: Argument<i32>,
          W: Argument<i32>
{
    t.value() + u.value() + v.value() + w.value()
}

struct ExampleArgument(i32);

impl Default for ExampleArgument {
    fn default() -> Self { ExampleArgument(40) }
}

impl Argument<i32> for ExampleArgument {
    fn value(self) -> i32 { self.0 }
}

fn main() {
    let t = 10;
    let u = Some(20);
    let v = None;
    let w = ExampleArgument::default();
    println!("{:?}", example(t, u, v, w));
}

You can run it at http://is.gd/oMbPrX

Conceptually, the default value should go with the parameter not its type. With this setup, every default value requires its own data type. This is a lot of typing (both kinds) and I don’t think it puts ideas in the right place. Compare:

// Adding trait for demo because I can't implement write the blanket Into
// or From impl necessary.
trait IntoOption<T> {
    fn into_option(self) -> Option<T>;
}

impl<T> IntoOption<T> for Option<T> {
    fn into_option(self) -> Option<T> {
        self
    }
}

impl<T> IntoOption<T> for T {
    fn into_option(self) -> Option<T> {
        Some(self)
    }
}


fn example<T, U, V, W>(t: T, u: U, v: V, w: W) -> i32
    where T: IntoOption<i32>,
          U: IntoOption<i32>,
          V: IntoOption<i32>,
          W: IntoOption<i32>
{
    let t = t.into_option().unwrap_or(0);
    let u = u.into_option().unwrap_or(0);
    let v = v.into_option().unwrap_or(0);
    let w = w.into_option().unwrap_or(40);
    
    t + u + v + w
}

fn main() {
    println!("{:?}", example(10, 20, None, None));
}

playground

I have been thinking recently about the same thing. My conclusion was that my need for such feature goes away if there is a way to specify default values for parameters.

In other words, I tend to do the following:

fn do_something(t: i32, u: i32, v: i32, timeout: Option<i32>) {
     let timeout = timeout.unwrap_or(42);
     /// code goes here 
}

fn main() {
    do_something(3, 4, 2, None)
}

which will be unnecessary with an hypothetical syntax like

fn do_something(t: i32, u: i32, v: i32, timeout: i32 = 42) {
     /// code goes here 
}

fn main() {
    do_something(3, 4, 2)
}

I don’t think that C++ style default parameter values will fit well in Rust. We don’t have overloading except via traits, while C++ already allowed type and argument number overloads.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.