Proposal: Multiple guards per pattern

Is there any existing proposal about allowing multiple guards on a single pattern in a match? I’m not using Rust in practice much, but I’d imagine using guards might become repetitive:

fn main() {
    let y = Some(42);
    let result = match y {
        Some(x) if x > 0 => "some positive",
        Some(x) if x < 0 => "some negative",
        Some(x) => "zero",
        None => "unknown",
    };
    println!("{}", result);
}

The repetition of Some(x) would be problematic if the pattern was more complex. Coming from Haskell which also has pattern matching and guards and allows multiple guards per pattern, I’d enjoy syntax like this to become valid in Rust:

fn main() { /* Variant 1 */
    let y = Some(42);
    let result = match y {
        Some(x)  if x > 0 => "some positive",
            else if x < 0 => "some negative",
            else          => "zero",
        None => "unknown",
    };
    println!("{}", result);
}

While this particular example could be replaced also with

fn main() {
    let y = Some(42);
    let result = match y {
        Some(x) => if x > 0 {"some positive"}
                   else if x < 0 {"some negative"}
                   else {"zero"},
        None => "unknown",
    };
    println!("{}", result);
}

this is no longer an alternative if the guards are not exhaustive.

fn main() { /* Variant 1, non-exhaustive */
    let y = Some(42);
    let result = match y {
        Some(x)  if x > 0 => "some positive",
            else if x < 0 => "some negative",
        _ => "something else",
    };
    println!("{}", result);
}

Alternative Syntax Ideas

Since the else above pretty much serves as a way to repeat a pattern without duplicating it syntactically, maybe some ellipsis sign like .. (or something else) might be nicer

fn main() { /* Variant 2 */
    let y = Some(42);
    let result = match y {
        Some(x) if x > 0 => "some positive",
            ..  if x < 0 => "some negative",
            ..           => "zero",
        None => "unknown",
    };
    println!("{}", result);
}

or maybe nothing at all

fn main() { /* Variant 3 */
    let y = Some(42);
    let result = match y {
        Some(x) if x > 0 => "some positive",
                if x < 0 => "some negative",
                         => "zero",
        None => "unknown",
    };
    println!("{}", result);
}

well, that completely empty LHS of => might be a bit too much, so:

fn main() { /* Variant 4 */
    let y = Some(42);
    let result = match y {
        Some(x) if x > 0 => "some positive",
                if x < 0 => "some negative",
                else     => "zero",
        None => "unknown",
    };
    println!("{}", result);
}

which is getting close to Haskell, where you’d be using guards in a syntax resembling this:

fn main() { /* Variant 5 */
    let y = Some(42);
    let result = match y {
        Some(x) if x > 0 => "some positive",
                if x < 0 => "some negative",
                if true  => "zero",
        None => "unknown",
    };
    println!("{}", result);
}

This is however, perhaps, problematic regarding exhaustiveness checks.

Comparing to Haskell

For anyone unfamiliar with Haskell, here’s an example:

{-
Note: Haskell doesn’t have exhaustiveness checks (except as optional warnings)

The example below is using a definition from the standard library: 
otherwise = True
[Read: “const otherwise: bool = true;”]

And it is using Haskell’s type `Maybe` which translates as follows:
    Maybe   ≙ Option
    Just    ≙ Some
    Nothing ≙ None
-}

main = do
  let y = Just 42
  let result = case y of
        Just x | x > 0     -> "some positive"
               | x < 0     -> "some negative"
               | otherwise -> "zero"
        Nothing -> "unknown"
  putStrLn result

Thanks for reading this and giving feedback ^^

2 Likes

I want to prefix this by saying I don't have a stong opinion on this feature either way -- I don't love it or hate it. But the motivation for it is not very strong. Repetition is by itself not a problem to solve, because the vast majority of people use editors with copy/paste capability, so unless the repetition is hurting readability, I don't see what it is really helping. If some indentation were being saved, that would be much more strongly motivating.

In fact, if your branches are long blocks, this has the potential to make things much worse:

fn main() {
    let y = Some(42);
    let result = match y {
        Some(x) if x > 0 => {
            ... some long code
            ... some long code
            ... some long code
            ... some long code
            ... some long code
            ... some long code
            ... some long code
            ... some long code
            ... some long code
            ... some long code
        },
            else if x < 0 => "some negative", // Uh oh, what is the pattern?
        Some(x) => "zero",
        None => "unknown",
    };
    println!("{}", result);
}
1 Like

Regarding indentation (perhaps not what you were after when mentioning indentation):

I’m not sure what the best approach would be: should the else be indented or perhaps on the same level as previous patterns. Same goes for potential other ellipsis marks. An un-indented version looks like:

fn main() { /* Variant 1 */
    let y = Some(42);
    let result = match y {
        Some(x) if x > 0 => "some positive",
        else if x < 0 => "some negative",
        else => "zero",
        None => "unknown",
    };
    println!("{}", result);
}

or with aligned ifs

fn main() { /* Variant 1 */
    let y = Some(42);
    let result = match y {
        Some(x) if x > 0 => "some positive",
        else    if x < 0 => "some negative",
        else             => "zero",
        None => "unknown",
    };
    println!("{}", result);
}

one further potential shortcoming being: The else might look like a default catch-all case, not related to the previous Some(x).

One important difference to Haskell might be, that Rust users use a tool like rustfmt that (AFAIK) kills manual alignment, whereas the guards in Haskell are kind-of designed to put those vertical bars on top of each other (or at least ideomatically used in such a way).

Proper vertical alignment despite using rustfmt might be possible like this:

fn main() { /* Variant 4 */
    let y = Some(42);
    let result = match y {
        Some(x)
            if x > 0 => "some positive",
            if x < 0 => "some negative",
            else => "zero",
        None => "unknown",
    };
    println!("{}", result);
}

One way to resolve this is with an immediately invoked closure

(|| {
    match y {
        Some(x) =>
            if x > 0 { return "some positive" }
            if x < 0 => { return "some negative" }
            // x == 0 falls through here to the end
        }
        None => (),
    }
    
    "unknown"
})()

Or if you need control flow, use a loop with break values.

loop {
    match y {
        Some(x) =>
            if x > 0 { break "some positive" }
            if x < 0 => { break "some negative" }
            // x == 0 falls through here to the end
        }
        None => (),
    }
    
    break "unknown";
}
2 Likes

In the specific case brought up you can write:

fn main() {
    let y = Some(42);
    let result = match y {
        Some(1..=i32::MAX) => "some positive",
        Some(i32::MIN..=-1) => "some negative",
        Some(0) => "zero",
        None => "unknown",
    };
    println!("{}", result);
}
2 Likes

One thing to be weary of is, ambiguous pattern variables I think rust has all the necessary things already for this, but not thought about the details of this proposal on top of that.

Thanks for the link. I’ve seen something about or-patterns with guards (especially regarding guards with side-effects) in the reference or somewhere, but totally didn’t think about them here yet.

Alternative idea:

let result = match y {
    Some(x) => {
        if x > 0 {
            "+ve"
        } else if x < 0 {
            "-ve"
        } else {
            fallthrough;
        }
    }
    _ => "else"
};

I’ve thought of that, I think this doesn’t fully work – or at least is more complicated than one might first think. Now that I’m thinking about it again, the workaround by @Yato might have this shortcoming, too.

The point being: When a match expression is done by value, when you’re inside the guard expression, the scrutinee is not decomposed yet, and the matched variables only contain shared references. Only after a guard passes, when you are fully commited to that case, destructuring (i.e. moving out of the scrutinee) takes place, and at that point it is not possible anymore to do such a fall-through.

Relevant section in the reference, see the last paragraph unter “Match guards”.

A similar but less fundamentally important thing applies with mutable references, currently the guard only sees immutable ones in that case, probably just to underline the point that a guard is not really supposed to have side-effects.

As to whether this means this approach doesn’t work or is just making things more complicated: The approach might work by doing static analysis that on the path to any fallthrough only immutable references to the matched variables are used. That’s some novel kind of static analysis AFAIK not something done by the compiler anywhere yet, but sounds feasible. If done this way, it has the benefit of being even more flexible than what I proposed.

I really don't like this idea. I am very glad that rust's match blocks don't support fallthrough.

3 Likes

We could keep consuming the bound variable, but require that the next arm not to have any bindings and cannot refer to the matched variables. Basically,

match x {
    ...
    Pat1(y) => {
        if stuff(&y) {
            consume_1(y);
        } else {
            consume_2(y);
            fallthrough;
        }
        more_actions();
    }
    Pat2(_) => {
        do_something();
        do_something_more();
    }
    ...
}

should be equivalent to

match x {
    ...
    Pat1(y) => {
        let should_fallthrough = loop {
            if stuff(&y) {
                consume_1(y);
            } else {
                consume_2(y);
                break true;
            }
            more_actions();
            break false;
        };
        if should_fallthrough {
            // copy from (or `goto`) the arm below
            do_something();
            do_something_more();
        }
    }
    Pat2(_) => {
        do_something();
        do_something_more();
    }
    ...
}

Please elaborate? While I'm not seriously pushing for this idea, we should note that this is an explicit fallthrough which is different from C's implicit fallthrough in their switch statements.

1 Like

Sure, I know that it is an explicit fallthrough. An implicit form would be obviously much worse.

I don't like even an explicit form because I think that it is a hint of bad software design. Since it either means that an or pattern in the match would be better or it too closely ties the branches with their ordering. (I know that the order of patterns does matter).

Of the variants discussed, I most like the one with just if and optionally else, but no else if (in other cases it provides disambiguation over plain if, which is not required here). I also think it should be block-indented like all Rust code:

// exhaustive guards
match y {
    // the pattern goes on its own line as soon as there's more than one guard
    Some(x)
        if x > 0 => "some positive",
        if x < 0 => "some negative",
        else => "zero",
    _ => "unknown",
}

// non-exhaustive guards
match y {
    Some(x)
        if x > 0 => "some positive",
        if x < 0 => "some negative",
    _ => "something else",
}

I consider the exhaustive variant to be quote a bit more readable than the same without guards:

match y {
    Some(x) => {
        if x > 0 {
            "some positive"
        } else if x < 0 {
            "some negative"
        } else {
            "zero"
        }
    }
    _ => "unknown",
}

Fallthrough may be an interesting feature but it's a very different can of worms, it probably should be discussed in a separate thread.

As for patterns, instead of using if for more purposes, how about expanding the notion of pattern guards to allow a nested match and permit such a nested match to be non-exhaustive?

match y {
    Some(x) match x.cmp(&0) {
        cmp::Greater => "some positive",
        cmp::Less => "some negative",
        // Note: not exhaustive, continue matching the outer pattern.
    },
    _ => "something else",
}
5 Likes