Matching optionals

I just come up with an idea to improve ergonomic on matching. What if we can write

let tmp: (Option<i32>, Option<String>) = match result {
    Ok(v?) | Err(e?) => (v, e),
};

rather than having to write

let tmp: (Option<i32>, Option<String>) = match result {
    Ok(v)  => (Some(v), None),
    Err(e) => (None, Some(e)),
}

? It would be easily to see how this idea can be extended to other enums and more complicated patterns.

This idea was came from this post.

More example:

let tmp: (Option<[u8]>, Option<IOError>) = match asyncresult {
    Ready(Ok(data?)) | Pending | Ready(Err(err?)) => (data, err),
}

which desugar to

let tmp: (Option<[u8]>, Option<IOError>) = match asyncresult {
    Ready(Ok(data)) => (Some(data),None),
    Pending => (None, None),
    Ready(Err(err)) => (None, Some(err)),
}

we could also write

let Ok(result?) | Err(err?) = result;
if result.is_some() {
    success(result?)
} else if err.is_some() {
    fail(err?)
}

so the solution in the post above can be written

pub fn flatten_nested_results<T, E, II, IO>(iter_outer: IO) -> impl Iterator<Item = Result<T, E>>
where
    II: Iterator<Item = Result<T, E>>,
    IO: Iterator<Item = Result<II, E>>,
{
    iter_outer.flat_map(|iter_inner_result| {
        let Ok(v?) | Err(e?) = iter_inner_result;
        v.into_iter().flatten().chain(r.map(Err))
    })
}
2 Likes

I don’t see how that transformation works. What does ? do exactly?

3 Likes

v? in pattern mattching means an “optional match”, if the arm hits, v have value Some(_) which wraps the matched value, otherwise it will be None.

This only makes sense when multiple arms are combined with |, of cause.

So v? is another way to get around E0408.

The type analysis: v is a variable that have type T: Try where <T as Try>::Error: Default, and v? : <T as Try>::Ok. So v can appear in some other match arms without the question mark, as long as it inferences to a type that is not conflicting. When there is no other clues for inference, T is default to Option<_>.

There are multiple reasons I’m against this proposal:

  • The semantics of this new ? operator are different from the one Rust already has. This is not in line with the principle of least surprise, in fact it’s the very opposite.

  • By your own admission in the first post, what you want can already be done in current-day stable Rust. It’s just too long for your tastes I think.

  • It makes the patterns in a match expression cryptic, as both @RustyYato and I can attest. At the very least the obviousness of the meaning of the pattern is lost.

  • It makes the Option type special in the eyes of the language. This is something to be avoided when and where possible, as the long term benefits are unlikely to offset the cost.

9 Likes

v? in patterns tells the compiler try to find a value to fill in, if no such a value, use the default value of the error type.

I think the above should be intuitive enough. Every new feature requires some time to be familiar with.

For pattern matchs, there are already some such grammars exist: ranges, bindings, and if clauses are all features that require some time to learn. There will be more if the ! pattern being added to the language. So I didn't see why this usage of ? cannot be used with good.

So what I proposed in a reply is to suggest using Try trait (with a requirement that its Error type must implement Default), and I have did as many as I can to make it consistent with the current use of ? operator.

1 Like

I understand what it means now, but it doesn't change my point that it overloads syntax that's already used to a rather different effect. Reusing the question mark operator seems like a recipe for creating both confusion and bugs, in a similar fashion as when the + operator would be reused for matrix multiplication (yes, I'm aware that * is also overloadable). So the issue is not the fact that new syntax is added: it's the fact that your proposal blurs the meaning of the ? operator.

And intuitive it is definitely not: intuitive means someone figures it out from context alone. On the other hand, it was necessary for you to explain what exactly the question mark was doing there.

As for consistency: if the expression in expr? matches None, does your version of the ? operator jump out of its current function/method? If not, it's not consistent at all. But if it does, the proposal as it exists is fundamentally broken, as the block mapped to the match pattern will not be executed.

2 Likes

My intension is to make it clear that ? turns a v:T where T: Try into v?: <T as Try>::Ok. So if it is in the expression position, it means unwrap with potential control flow alter, and in pattern match position, it will be wrapped over.

In pattern match position things can be different though. Example: if in statement have the consequence following it, and in pattern match the consequence is in front of it.

The way I proposed for the ? operator makes sure that Ok(v?) | ... => Ok(v?) is a valid match arm, which means the pattern match uses exact the same form as the expression to generate the value. I think this was somehow people want to have.

At this point all patterns remove some layer from the value, but the never add things. This will be the first pattern that adds something.

How would type inference work, I suspect it won't work for this. If you want to limit it to just Option, that would need to make Option a lang item, not just a library item, which is undesirable.

ref adds a layer of indirection.

2 Likes

Ah, yes, I forgot about that. Although I think that ref is a bit special because it has to do with builtin references (which cannot be just a library type in the same way that Option is).

2 Likes

Generally, when I’m pattern matching, I need to get something out of an Option; making a new Option means I’ll just have to do another extraction of some kind.

4 Likes

Read differently: if you have patterns that benefit from this feature, your patterns are too complicated.

1 Like

Emm... Let me take a look at this claim.

The requirement of my match on Result<T,E> is trying to turn this enum into a flat tuple that preserve all information.

In theory, all enums can be turned into a tuple like this, as long as we have Option type. This the way Go goes, and obviously it have its own use although I believe Rust's way is better.

So if we have Go program to port, making this enum-to-tuple conversion easy is a big benifit because it makes the translation of logic much easier.

Even without it, the ability to turn an enum into a tuple can still be benificial like the example given - The original Reddit post writer tried to return impl Trait from different match arms, but this does not work.

Instead of using Box<dyn Trait> or Either, this gives another elegant solution and I didn't realised when I have the same issue multiple times when using tokio, until I successfully solved that puzzle with my new solution.

This idea can be make more general: as long as we have enough good functiosn for Option to make things easy without pattern match on it.

So yes, I believe this pattern would have been used offen enough to justify, as today many real world code suffer or lack of hintsight on the possible to use this.


fn operation1() -> impl Future<Item=(), Error=()>{
    ...
}
fn operation2() -> impl Future<Item=(), Error=()>{
    ...
}
//Not compiling!
//fn operation3(condition: bool) -> impl Future<Item=(), Error=()> {
//    if b {
//        operation1()
//    } else {
//        operation2()
//    }
//}

//we only have to write this once! or we can put it in std
fn optional_future<T,I,E,Fu,F>(t: T, f: F) -> impl Future<Item=Option<I>, Error=E>
where Fu: Future<Item=I, Error=E>,
          F: FnOnce(Option<T>) -> Fu
{
// Use Either maybe?
// Or impl<F,I,E> Future for Option<F> where F: Future<Item=I, Error=E>
}

fn operation3(condition: bool) -> impl Future<Item=(), Error=()> {
    match (condition,()) {
        (true,t?) | (false,f?) => 
               optional_future(t, |_| operation1())
                    .join(optional_future(f, |_| operation2()))
                    .and_then(|_,_| {})
    }
}

So if I understand you correctly, you want to turn a value based on something like this:

enum Foo {
    Variant0,
    Variant1,
    Variant2,
    Variant3,
    Variant4,
    // possibly more variants
}

into something like this:

(Variant0, Variant1, Variant2, Variant3, Variant4, /*possibly more variants*/)

I'm hoping I've misunderstood you, because this approach doesn't scale: What if you have e.g. 255 enum variants? The enum values stay nice and small, but the output tuple becomes rather large: as each value in the tuple is let foosize = sizeof::<Option<Foo>>(), the tuple value as a whole is now 255 * foosize. More importantly, it's not necessary information: by principle of mutual exclusion only one of these tuple values should every be a Some(Foo::VariantX), which means information is needlessly being repeated in the tuple.

This is why I have this proposal - thanks to the flexible pattern match grammar, we can precisly choose which things we do want to match on. In your case, a typical match with this proposal would be

match (foo,()) {
    (Variant0,v0?) | (Variant1, v1?) |... => {
          //use v0..vn, all of those are of type Option<()>
    }
}

And if you only need to know whether it is Variant1 or Variant3,

match (foo,()) {
    (Varian1,v1?) | (Variant3, v3?) | _ => {
      ...
    }
}

You don’t have to write all those 255 enum variants!

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