Idea: an alternative syntax for `match`

I’ve been thinking for some time to an alternate of match which could cover both uses of match and if let, including if let chains. I’ve saw a lot of discussion on how to improve if let, but I think that they only exist because match is too verbose, and requires two level of indentation. If match was more lightweight, I do think all of those conversation would not be needed.

That being said I don’t think the gain is high enough to modify Rust in that direction, because even if the change can be completely automated, it would require every Rust project ever to be updated, and be even more disruptive than the introduction of the ? operator. That’s why I’m not posting on IRLO, but here.

Anyway, what do you think of using a new contextual keyword with/else with which introduce alternative in a match, and using else for the diverging branch:

the enum `e`
enum E {
    A,
    B(i32),
    C(i32),
}
let e: E = …;

exhaustive match

match e {
    A => do_a(),
    B(b) => do_b(b),
    C(c) if c > 3 => do_big_c(c),
    C(c) => do_small(c),
}

becomes

match e with A {
    do_a();
} else with B(b) {
   do_b(b);
} else with C(c) if c > 3 {
    do_big_c(c);
} else with C(c) {
    do_small_c(c);
}

partial match

if let A = e { do_a() };
if let B(b) = e { do_b() };

becomes

match e with A { do_a() } else {}
match e with B(b) { do_b(b) } else {}

partial match with divergent branch

(note: is divergent the right word?)

if let A = e {
    do_a();
} else {
    do_default();
}

becomes

match e with A
    do_a();
} else {
    do_default();
}

partial match with capture of the diverting branch

(IIRC something like this is currently being proposed but is not yet accepted)

if let Ok(foo) = some_fallible_function() {
    do_foo(foo);
} else error (
    report(error);
}

becomes

match some_fallible_function() with Ok(foo) {
    do_foo(foo);
} else with error {
    report(error);
}

chaining of partial match

(note: I forgot if it’s already accepted)

let f: E = …;
if let B(e_b) = e && let C(f_c) = f {
    do_b_and_c(e_b, f_c);
}

becomes

match (e, f) with (B(e_b), C(f_c)) {
    do_b_and_c(e_b, f_c);
} else {}

note: if order to have the exact same semantic, if f in an expression in match (e, f), it must be lazily evaluated only if B(e_b) is correctly matched.


Remarks:

  • match … with … { … } else {} could also be spelled partial match … with … { … } but this requires the introduction of a new contextual keyword.
  • match (e, f) with (B(e_b), C(f_c)) { do_b_and_c(e_b, f_c) } else {} (the last example) could be spelled lazy match (e, f) with … but it also requires a new contextual keyword.

Is it different from 3137-let-else - The Rust RFC Book which has already been stable since 1.65 ?

This does exactly the same as the already existing match expression. We shouldn't introduce even more redundant ways of writing the same thing with a different spelling. Hard no.

5 Likes

I don't like the proposal to turn a match block with clearly separated arms into a chain of conditionals. The latter are much less readable at a glance, and personally less visually appealing. Modern languages go in the reverse direction, where special-purpose constructs are introduced to avoid chaining if-else specifically. For example, that's a large part (but not all) of motivation for the new match statement in Python. Kotlin has when statements, which can be used without a scrutinee just to group if-else chains of conditionals into something more readable. Odin has switch statements, with similar semantics.

Besides the aesthetic reasons, the current match blocks explicitly list the checked conditions, and thus can be statically verified for exhaustiveness. With your proposed syntax, pure matches and abritrary function calls are intertwined, so checking exhaustiveness becomes impossible, at least in practice, where nothing would guide the programmer away from entangling declarative and procedural conditions.

Outside of the chaining, I don't really see a benefit over the current constructs. Your code looks more verbose, with dangling empty blocks. A dangling block is always problematic, because you never know whether the author intended to leave it empty, or just forgot to do it. With let-else, destructuring let and even if-let the intent is unambiguous.

2 Likes

You can already write this with the current match:

match (e, f) {
    (B(e_b), C(f_c)) => do_b_and_c(e_b, f_c),
    _ => {}
}

However let chains were added because they also allowed using the bindings introduced by earlier patterns in the expression to be matched later. For example:

if let B(e_b) = e && let C(f_c) = e_b.f() {
    do_b_and_c(e_b, f_c);
}

For this you need two match, both with the current match or the match with you're proposing.

2 Likes

I know, it’s why I specifically didn’t posted in IRLO. And to remove the redundantness, both if let and current match should be deprecated, which would introduce way too much churn to be a viable alternative, as I said in the introduction. This was not a serious proposal, but not an exploration of an alternative that could have been taken in the past.

I wasn’t clear, but the explicit else part would be required (or using partial match) otherwise it shouldn’t compiles. That being said, I don’t see how it’s more intertwined than current match. If you refer to the if c > 3, it’s just something that can do with current match, I’m not introducing new way to match of the variants.

I was also removing symbols (=>) since it’s another critique that Rust often receives, but I guess you would have preferred a more compact syntax like that.

match e with A => do_a();
else with B(b) => do_b(b);
else with C(c) if c > 3 => do_big_c(c);
else with C(c) => do_small_c(c);

That’s why I also proposed partial match…, but yes I do agree with that objection.

Thanks! That was what I was missing. And indeed it would requires to introduce yet another construct, like

match e with B(e_b) && e_b.f() with C(f_c) => …

At that point my proposal doesn’t simplify anything anymore (by superseding both current match and if let), since we still have as much different construction than before (if let, il let … else …, if let … && let that becomes match with => … else {}, match with … else … and match with … && with … => …), and at that point, it’s just that I prefer to have matching semantic using the keyword match than the keyword if and left-to-right association instead of right-to-left, but frankly it’s just another green color for my bike.

So thanks everyone for your time, I was very constructive.