Pre-RFC: syntax sugar for `matches!`

To my mind, the infix matches is the obvious choice. If you know what a match statement is, the meaning of if foo matches Ok(0) { is immediately clear without learning any new rules.

I certainly wouldn't want a new sigil for this, especially not one with ~, which has many other meanings, like "approximately" and "not". The slightly shorter code can't possibly be worth the downside of adding one more thing for new users to stumble over.

6 Likes

I think that, if a language construct were to be added to replace the matches! macro, this would read quite nicely, even if the keyword matches is a bit long by Rust's standards (compare fn, impl, type, let, etc).

However what could make this worth it for me is the orthogonal feature of being able to pattern match while also using && to create larger boolean expressions in an if-style block. Currently this is possible in a match expr but it brings with it 2 levels of indentation for the match expression arms, which is not always desirable.

So basically I'd like something like this to be possible:


let opt = Some(42u8);

if let Some(num) = opt && num != 42 {
    // take action
} 

But that brings with it its own design issues w.r.t. composability of boolean expressions.

What about struct literals? matches! gets around any parse ambiguity because has surrounding delimiters.

What is the meaning of the following?

if foo { bar } matches baz { bar } { bar }

Think about what the parser needs to know for that to work, and what a human will have to do to understand it.

We already have a similar case when it comes to if and struct literals where we recover somewhat gracefully, but it is an ugly hack that I would like to avoid infecting other parts of the grammar.

8 Likes

I already address this issue in the OP. This feature does not include variable bindings.

Nothing left to design here, the RFC has landed almost 2 years ago.


And in the meantime, just in case it wasn’t clear, matches! support guards:

if matches!(opt, Some(num) if num != 42) {
    // take action
} 
3 Likes

Is there another way to parse this besides

if matches!(foo{bar}, baz{bar}) { bar }

?

Yes.

if matches!(foo { bar }, baz) {
    bar
}
// next statement:
{
    bar
}
1 Like

how could it even know? It’s 100% ambiguous:

#[derive(PartialEq, Eq)]
struct foo { bar: () }
type baz = foo;
const baz: foo = foo { bar: () };
const bar: () = ();

fn main() {
    // if foo { bar } matches baz { bar } { bar }
    if matches!(foo { bar }, baz) { bar } { bar }
    if matches!(foo { bar }, baz { bar }) { bar }
}

(playground)

Funnily enough it actually works on stable. Haven't figured out which one it's using yet (playground)

By choosing an interpretation.

fn main() {
    if let foo { bar } = baz { bar } { bar }
    if let foo { bar } = (baz { bar }) { bar }
    if let foo { bar } = (baz) { bar } { bar }
}

(playground)

The warnings (if not surpressed) point out the clarifications you can make to the code to remove the ambiguity and move it towards what the compiler is interpreting it as.

I believe the way the "brackets not allowed in expression of if" warning works is that if the name used is the name of a type but not of a value, the hint is emitted along with the name lookup error.

I feel like we should get if-let chaining (with bindings and &&) to work first. That gives enough expressive power to solve this problem, and makes it easy for a macro to reverse the syntax if someone wants the pattern on the right. If in practice people often want the pattern on the right, we could then consider adding something like is or matches as a reversed let.

4 Likes

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

Can you catch me up to speed on the is vs matches debate? I have previously made the following comment:

if let chaining will mostly solve the problem, but:

  • I find the if let syntax rather ugly, and beginners are regularly confused by it
  • Writing the expression after the pattern often feels unintuitive, this is often compared to Yoda speech
  • An expression that evaluates to bool and can introduce bindings would be more powerful than if let, because it can be used outside of if expressions. For example, it can be used in an if guard, in Iterator::filter or str::contains:
let b = "hello world!".contains(|c| c matches 'a'..='z');

Of course, this is what matches!() exists for, but

  • An infix keyword would be more intuitive; to use matches!() you have to know if the expression comes first or the pattern.
  • An infix keyword is much more readable, especially if the expression is long. For example:
    iter
        .map(...)
        .filter(...)
        .next()
        matches None
    // vs.
    matches!(
        iter
            .map(...)
            .filter(...)
            .next(),
        None
    )
    
  • It's better to have one syntax that works everywhere, rather than having to choose between if let and matches!().

A bool expression would also enable new code patterns, e.g.

x matches Some(y) || return 0;
println!("{}", y);

If x is Some(_), then the value is bound to y; otherwise, the function returns. Since return evaluates to !, which can be coerced to bool, the types check out.

The current alternatives to this pattern either require an additional level of indentation, or unwrap():

if let Some(y) = x {
    println!("{}", y);
} else {
    return 0;
}
// or
if let None = x {
    return 0;
}
println!("{}", y.unwrap());
2 Likes

Happy cake day btw!

2 Likes

I have already said to you "I find binding after the =~ sign to be confusing and comparable to yoda speech. I would rather all bindings to be on the left side." here. Could we continue this discussion on that thread so the context is available to those trying to catch up?


Also dunno if you saw, could you please respond to my comment about bindings in match arms?

Both x and y would be available in the match arm. But note that I'm not a fan of the ~ syntax either.

So, I do think that something like matches makes sense. I'm not suggesting we shouldn't do it. But we already have if let, and extending that to allow chaining would be an intuitive extension for people who are already used to if let; I think we should do that regardless. Then, we could define matches as a trivial desugaring, with if let taking care of details like the scopes of bindings.

I wouldn't want to have matches without having bindings, because it's incredibly useful to write things like if expr matches Some(x) && another_expr(x). That has all the same issues as if let, in terms of the scopes of the bindings. I think we should have both, and I think we should define the semantics only once.

I like your examples. They make me feel like any form of method-position macro would be the optimal solution.

let b = "hello world!".contains(|c| c.matches!('a'..='z'));
iter
    .map(...)
    .filter(...)
    .map(...)
    .next()
    .matches!(None)



This kind of syntax was deemed suboptimal in another thread (I don’t remember which one) (Edit: It was in a few of these 160+ comments on GitHub) because it binds something on the right hand side which is rather confusing:

x matches Some(y) || return 0;
println!("{}", y);

I could personally perhaps live with something along the lines of

if !let Some(y) = x { return 0 }
println!("{}", y);

but that’s not too pretty either. Maybe another method macro

let y = x.unwrap_or_else!{
    return 0;
};

might come in handy in a bunch of situations. Or use try blocks?


There’s also this terrible wonderful solution for the time being:

let y = if let Some(y) = x { y } else {
    return 0
};
println!("{}", y);

I guess with a sophisticated macro that ... well ... guesses based on case (or is there any other way for proc macros perhaps?) which of the identifiers are variables and which ones are constants, that automates this extraction process so you can write

let_or_else!{Some(y) = x {
    return 0
}}

still ugly...., perhaps:

let_!{ Some(y) = x, else:
    return 0
}
1 Like
x matches Some(y) || return 0;
println!("{}", y);

This causes some weird lifetime and scoping issues. Would y be a reference to x? What would its lifetime be?

What about this?

let b: bool = x matches Some(y);

You could tie y to live as long as b. That would make this invalid:

x matches Some(y) || return 0;
println!("{}", y);

But this would be valid:

let b = x matches Some(y);
if b {
    return 0;
}
println!("{}", y);