Pre-RFC: syntax sugar for `matches!`

In my opinion, the fact that matches!() can be implemented purely as a macro without any problems indicates exactly that it should not be a language feature.

It's already syntactic sugar anyway, basically – it stands for a match with a catch-all that defaults to false. The advantages (if any) of having it as a builtin feature would be marginal, but it would be heavily redundant, because pattern matching already exists in the language.

6 Likes

Yeah, think this is not really an issue, but rather the artifact of current implementation which we’ll need to fix for libraryfication anyway.

The core problem is that the „text -> toke trees -> ast“ model is a lie.There‘s no such thing as universal token tree format, because proc macros and macro by example already are using TTs of different shapes. Now, the rest of the compiler „happens“ to use mbe-style token trees, but that’s a pretty ad hoc model.

The right way to think about TTs is as an interface between compiler and macros. When expanding a macro, compiler needs to lower its internal representation to the TT format, appropriate for the macro. The knowledge that $tt matches == but not ~= should be the part of this lowering layer.

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.