QOL improvements to Pattern matching and let else

Hello rustlings! I have an idea, so a lot times when i find myself using a let else i want to get the value of the expression that didnt meet the requirements of the let part so im forced to use a match instead which is quite inconvenient!

So i propose allowing a exaustive pattern in the else branch of the let else.

let Ok(foo) = bar() else Err(fizz) {
  ...
};

you could even have this

let Outcome::Success(a) = func() else Outcome::Retry(e) | Outcome::Fail(e) {

};

or this

let Some(val) = list.get(0) else foo {

};

As all it needs is an exaustive pattern.

I believe this is a much simpler alternative to ideas previously shown here like let else match and even catch, this would add no new keywords and in my opinion its quite logical.

3 Likes

You'd need a => delimiter, like in match arms:

let Some(val) = list.get(0) else foo => {

};

otherwise the syntax is ambiguous (foo {} could be a struct pattern).

But honestly, I'm not a fan of this. There are already 3 ways to pattern-match (match, if let and let else), but it's okay because they have slightly different use cases. When you need to access both enum values, it's best to use match. And sometimes you can use a helper method such as Result::map_err and ?.

It would be nice to have Kotlin's inline functions:

sealed class Result<T, E> {
    class Ok<T, E>(val value: T): Result<T, E>()
    class Err<T, E>(val error: E): Result<T, E>()

    inline infix fun or(branch: (error: E) -> T): T = when (this) {
        is Ok -> value
        is Err -> branch(error)
    }
}

This works like Rust's unwrap_or_else function:

val result: Result<Int, String> = Err("Hello world")
val num = result or { 42 }
val len = result or { err -> err.length }

But the difference is that the lambda is allowed to yield control flow to the caller:

val num = result or { return 42 }
// or
for (result in list) {
    val value = result or { continue }
}
1 Like

Oh yeah sorry I forgot about that, it's just that currently the way let else is structured rn makes it a lot less useful, but you could instead of a pattern just have a binding altough there would be some issues with pattern matching making it only really viable for enums with only two variants which at that point it would be better to add a catch operator. As for inline functions you can use macros

@Aloso is right, we don't need more syntactic sugar, even though when I first looked at this I couldn't tell that it wasn't valid Rust. You know I've also had an idea for error handing (option || return, but it was just a one-liner equivalent of let ... else {}). People said the same thing, you already can do that and there's no need for more syntactic sugar. In fact, it's bad to have tons of syntactic sugar.

My preferred alternative mentioned many times in the past is let-else-match, to make it actually handle more than two patterns nicely.

let Ok(Some(foo)) = bar() else match {
    Ok(None) => panic!("missing element"),
    Err(err) => panic!("error {err}"),
};

(if anything is to be done, I'm sort of on the fence that I have had to do let foo = match bar() { Ok(foo) => foo, ... }; far too many times, but the sugar isn't that much nicer).

17 Likes

what about using posfix match?

With flow typing the example can be

let Ok(foo) = res else {
    let Err(fizz) = res;
    ...
};

but that doesn't help the let Ok(foo) = bar() case at all. In most cases with error handling I am trying to push the error handling logic up some levels, this line would usually read let Ok(foo) = bar()?. Try blocks should help organize this within functions.

In almost all cases outside of error handling, I would prefer this became a match. Error handling specifically in rust is in a constant state of evolution, it would be nice to have an "end goal" version to see if let-else-match makes sense in that world.

IMO the main advantage of let-else-match over a match with only 1 branch that doesn't diverge is that it makes it clear that all the other branches diverge.

I've had code where there's several different match arms, of which one gives a value (which I assign to a variable) and the rest diverge. For enough match arms that are long enough, it becomes non-obvious that this is what's going on, and a quick skim could misread a diverging arm as non-diverging or the other way around.

If we had let-else-match, then cases where only one arm doesn't diverge become easy to annotate as such.

12 Likes

A lot of the toy examples also work nicely only for enums with exactly two cases, when you are matching on exactly one of them. As soon as your else case involves multiple patterns, that goes out the window. else match { does not have that problem.

This isn’t inherently an issue; Rust has already decided that it’s worth adding some special syntax to deal with Result-like enums (?). But I’d still be inclined to prefer the more general feature here. (And yeah, it isn’t strictly necessary. But it doesn’t seem harmful either, no more than let else was in the first place, and it does bring the positives of let else to more situations.)

3 Likes

Well I see the issue with my proposal but what about a kind of shorthand that lets you bubble up the result

let foo = match may_fail(){
  Ok(super),
  Err(err) => todo!(),
};

This current syntax might or might not be ambiguous so it might need to be changed, but afaik super ain't a pattern keyword.

2 Likes

if we're talking about syntactic sugar removing the need for duplicate variables, i would rather have one for |x| x.m() closures first.

also, it's worth noting your proposed syntactic sugar is actually longer than the status quo. and it feels weird having a kind of pattern that is only valid in match statements.

1 Like

Whats duplicated about that? I mean I see the name x being used twice, but with very different purposes: the first occurrence binds a name to a value, the second one references that bound value using the bound name.

it's a duplicate variable in exactly the same way as Ok(x) => x is, it's a pattern and expression, tho in the closure case it's the pattern that's trivial, whereas before it was the expression that's trivial.

It may be trivial in form, but not in function: closures without parameter lists have been discussed.

The conclusion there was that it's not feasible in rust, because leaving out the parameter list has the potential to create binding ambiguities, which would then need potentially complex rules to resolve. That in turn would have the effect of making that part of the language more rather than less complex, and without any real gain to show for it.

Basically, closures are about as terse as they can feasibly get in Rust.

As long as no coercions are happening |x| x.m() can be rewritten as Type::m or if m is a trait method <_>::m even. To put it simply m is a one argument function being given to something that wants a one argument function, but the shortest way to name m is to wrap it in a closure that doesn't do anything. The shortest theoretical way to name m would be just m, though for namespacing reasons we'd might want some kind of sigil.

1 Like

I'm sorry but that's just not true, in that yes you can write it like that, but you can't use it like that. A simple counterexample is |x, y| {...}; i.e. a closure with multiple parameters.

Sometimes that's true, but more often than not, single-parameter non-coercing functions can just be used by name, without wrapping in a closure e.g. this works without any issues.

So you see, the shortest practical way to do it is also to just name the fn.

What does super mean here?

1 Like

Naming free function and method are two very different things because methods are namespace under types.

I don't see how a rarer more complicated example is relevant to discussion on sugar for a common simple case. Also it's not even a counterexample.

use core::ops::Add;
fn main() {
    let _: u8 = [0, 1, 2, 3].into_iter().fold(0, <_>::add);
}

This thread is about patterns and let-else, not closures. Please stop this entirely off-topic side-thread. :slight_smile: It doesn't make sense to "pitch" unrelated features against each other like that, and certainly not in an early spitballing phase.

5 Likes