Pre-RFC: syntax sugar for `matches!`

Rust 1.42.0 introduces the matches! macro to boolean test if a pattern matches an expression. This is a neat little macro. I find it useful when I want to use an if-let statement without actually binding anything. I personally have been using if-let statements for speed since I discovered this a couple months ago.

This Pre-RFC is to propose a syntax for matches! that would work as a drop-in replacement for == in cases where the left side expression matches the right side pattern.

Pros:

  • Encourages the use of the matches! function, leading to new design patterns and a faster idiomatic Rust

Cons:

  • Encourages the use of the matches! function, leading to preferences to a Rust-only syntax, similar to Javascript's === vs ==. There should instead be efforts in the compiler team to internally attempt to rewrite pre-existing == statements before defaulting to PartialEq.

Options for this syntax include:
=>: This borrows syntax from match (although in match it symbolizes destructuring). Would cause confusion in order of operations.
=~: The match operator from Perl.
~: Similar to the match operator from Perl but with requisites for macro_rules.
matches: Create a new keyword that can only be used inline in if statements.
is: See my complete and utter disdain for this syntax here


Further Arguments

  1. I would like this to include if-let bindings

I don't agree. I use the matches! syntax to fix this uncommon yoda case:

let x = 42;
if let 42 = x { /* stuff */ }

and adding bindings to matches! will make the common case yoda-like.

The lang team would like to wait and consider this after if-let chaining is merged.

Could you show some examples of this and how the proposal would improve it.

2 Likes
  • This was a placeholder for me while I still formulate my ideas, I have edited for clarity.

I'm imagining a situation similar to javascript's === vs ==, although the reasoning would be performance rather than correctness. Look at the following example:

fn src() -> Result<usize, usize> { /*...*/ }

fn foo() {
    if src() ~= Ok(0) {
        /* stuff */
    }
}

fn bar() {
    if src() == Ok(0) {
        /* stuff */
    }
}

If you view the MIR, foo has fewer memory accesses. I do not know MIR intimately, I do see that Bar creates 3 references and uses a promoted.

Another syntax that has been suggested before is expr is pat:

if src() is Ok(0) {
    /* stuff */
}

I find this more natural to read than matches, if let or a new sigil like ~=. I think it could even be added as a context-sensitive keyword, so it doesn't require a new edition.

However, I think this would only be worth it, if it would allow bindings, e.g.

if something() is Ok(n) && n > 4 && something_else(n) is Some(val) {
    println!("{}", val);
}

Because then if let could be deprecated in favor of is, which is more powerful, more general and more readable.

4 Likes

In My Opinion

The problem with is is that it's too general. If someone sees a new sigil then they know "ok this has a specific definition to Rust". is implies being. And has a lot of locality context. English speakers will lump more meaning onto it than non-native speakers and not on purpose, just through constant overuse.

IIRC the only language to get is right is Python because it compares the underlying pointers. If you really want to give Rust an is operator then it would need to be used for this case and this case only.

let x = 5;
let x_ref = &x;
assert_is!(&x, x_ref);

I see what you mean. I don't think it's a big deal, but we could use matches instead of is, if native English speakers find that clearer.

2 Likes

To address your other point

However, I think this would only be worth it, if it would allow bindings [...]

My RFC contains the line "I find it useful when I want to use an if-let statement without actually binding anything" as a direct argument for using matches! when you don't want to contend with if-let yoda syntax:

let x = 42;
if let 42 = x {
    /* do something */
}

I prepared an argument to defend separating the syntaxes :

Unfortunately it derives that using matches! for binding will lead to an uno reverse-reverse and not only cause yoda syntax, but cause yoda syntax in the much more common case

However I think arrow syntax actually makes this work

let myvar = Some(42);
if myvar => Some(x) {
    /* do something with x */
}

Thoughts?

=> conflicts with match syntax:

match expr {
    pat if cond => expr,
    ...
}

It conflicts with match syntax

I am not a compiler guru, is there any way to get a second opinion?

It seems odd that rules inside a match block also apply to statements outside of a match block but I understand that software architecture is never perfect and issues like this crop up from time to time. I'd like more detail on the problem and how easy it would be to work around but I also don't want to steal anyone's time if they do not wish to be invested in this argument.

One example where it's ambiguous is this match arm:

a if b => c => d

Which can be parsed as one of

a if (b => c) => d
// or
a if b => (c => d)

We could just say that it's left associative, so it's parsed like the second case, but I'd prefer a less ambiguous syntax.

2 Likes

Edited post for clarity

this has nothing to do with software. It is about syntax.


Not necessarily possible, at least not easily. The => from match is not just a binary operator, so this is not your usual operator expression parsing anymore. To make it even more complicated, the match-arm-=> is either PAT if EXPR => EXPR or PAT => EXPR while the matches-=> would be EXPR => PAT. Total madness to apply any kind of associativity.

Approaching a if b => c => d you would at least need to try parsing c => d as an expression which can either fail (when c is only valid syntax for a pattern) or succeed but then be immediately discarded because you actually need to parse it as a pattern followed by => instead (and make sure to discard it quickly, crucially before fully parsing d, otherwise you’re quickly in quadratic complexity land). Also imagine the inconsistency if the match arm a if b => c => d is supposed to mean a if (b => c) => d but the match arm a => c => d probably still means a => (c => d) (since the left hand side of the arm can only be a pattern).

And taking any anology to == would suggest to have no associativity but require parentheses anyways.

And taking any analogy to == would suggest to have no associativity but require parentheses anyways.

Yes. I would like the new matches! syntax to be analogous to ==.

Requisite note:

(Disclaimer: speaking as a member of the grammar wg but not on behalf of the group)

Adding new sigils is highly problematic for macro_rules! macros. E.g. >>= gets handled as a single token for macro_rules!, and must be matched as >>= and not as > > = or >> = or anything else. ~= is currently interpreted as ~ =, and the two literal sequences in source code are completely interchangeable in both macro definitions and use. Adding a new token to glue the two together would potentially be possible, but difficult and with surprising edge cases, in order to avoid breaking macro_rules! macros.

Only parsing the ~= token in a new edition mode would change the trade-off to be a decent amount less problematic, but it'd still be a surprisingly subtle edition difference and make calling macros cross-edition using ~= weird 202X->2018 (have to break it into ~ =) and impossible 2019->202X (as there is no way for edition 2018 to write the token if it's edition gated).

(Disclaimer²: I am not providing an opinion on new syntax for matching at this time.)

1 Like

fixed

Actually I think I understand. Any extra sigils would break custom syntax in somebody's macro_rules. I hate to bring up the try operator but I'm curious what path they took to avoid this.

? wasn't really problematic because it's a single character and wasn't already part of a multi-character operator. Multi-character operators are the challenge for the reasons mentioned above.

2 Likes

Regarding the analogy with Perl, it's '=~', not '~=', and there is also '!~' for "does not match".

2 Likes

Just a thought, how would you expect bindings to work inside a match arm?

I'm picturing something like

let myvar = Some(Ok(6));
match myvar {
    Ok(x) if myvar ~ Some(y) => { /* do something with x */ }
}

but I can also imagine something like

let var1 = Ok(7);
let var2 = Some(6);
match var1 {
    Ok(x) if var2 ~ Some(y) => { /* do something with x and y */ }
}

It seems like this issue interferes with a huge number of proposals for new syntax. Which in most cases is not much of a problem, since most proposals for new syntax are unjustified for other reasons. (I'm not a fan of this one.) Indeed, it's plausible that a need will never arise to add new sigils to Rust. But if one ever does, it will be too bad if this gets in the way.

Surely there's some kind of solution. Using >>= as an example, what if it were parsed as two tokens, but with a special case to make it an error if the tokens were not physically adjacent in the source file? Yes, there are edge cases (proc macros), and yes, it's a hack, but it should be doable. And Rust already has at least one hacky edge case around sigil tokenization: the fact that >> is sometimes treated as two closing brackets, despite being a single token. C++ used to force you to write > > in that situation, but people realized that it makes no sense when there's no real ambiguity in the syntax; it was fixed in C++11, and from the beginning in Rust. A similar principle should apply here.

3 Likes