Using `if let` to check for equality

Are you all aware you can use if let to check for equality? https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=7e7a155eb38d168d78365ff4f11e0f1e

Why does this work? Is it a quirk of pattern matching?

Patterns are patterns. There's no need to prevent people from using simple ones.

That's the same feature as match x { 1 => "one", _ => "something else" }. And can be useful in slightly more complex cases, like if let Ok(1) = my_result { when you have a non-PartialEq error type and thus can't use ==.

(Remember that if let A = B { C } else { D } is desugared into match B { A => C, _ => D }.)

6 Likes

Awesome, thanks a lot @scottmcm, I forgot about that desugaring.

(I think it's even preferable to use pattern matching above == when possible as it saves you from the bloat of #[derive(PartialEq)] and constructing the value to compare with == against, e.g. compare the MIRs in https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9dfbc17075a9595c94ab1506dd4772d4).

4 Likes

I wish we could just write expr is pat though :smiley:

9 Likes

Seconded. The inversion of the order in if let <y> = <x> does add some mental load for me.

4 Likes

matches!(expr, pat) already exists (and will be stable starting from 1.42).

4 Likes

I have mixed feelings about matches!.... Like, it certainly helps today, but I strongly feel that we need is which also introduces bindings into the part of the program dominated by the check (which at this point is such a boring idea that even Java has it). With is, matches! is just a technical debt.

6 Likes

Problem of having expr is Pat(binding) is always about the scoping of those bindings (referring to [Pre-Pre-RFC] Reviving Refutable Let [Runoff] again)

The JEP you mentioned is pretty short but skims lots of details, like how to determine the scope of y given x instanceof Type y. The actual implementation touches translation involving quite a number of constructs (&&, ||, !, ?:, if, while, do while, for), and contained some tests like these which I won't call them "boring" :upside_down_face:.

// Test for (!e).T = e.F
if (!(!(o1 instanceof String s) || !(o3 instanceof Integer in))){
    s.length();
    i.intValue();
}

// Test for (e1 ? e2 : e3).F contains intersect(e1.T, e3.F)
if (o1 instanceof String s ? true : !(o1 instanceof String s)){
} else {
    s.length();
}

There are two kinds of complexity here: cognitive complexity for the user, and implementation complexity for the implementor. My “boring” Java example refers strictly to the cognitive complexity. I agree that implementing this such that the user-visible behavior is obvious is non-trivial.

@kennytm
I have a prototype implementation that defines some scoping rules (for names, as in name resolution) precisely in two variants - 1) until the end of the block (pretty weird) and 2) until the end of the statement (better).

The variable may be actually accessible in a smaller scope than it can be named in, due to move checker enforcing initialization, that "scope" may be non-continuous and is defined by NLL.

(The prototype doesn't support ||.)

I think determining the scope of binding in Rust is similar to how uninitialized variables are tracked. The implementation complexity should already be a sunk cost (though explaining it in an RFC probably isn't :wink:)

Though I do mean expr is Pat(binding) has cognitive complexity in determining the scope of the binding when negation is involved.

if !(expr is Some(x)) {
    // x cannot be used here...
    // but we are inside a block where the parent declared x?
    return;
}
...
dbg!(x); 
// x is usable here... 
// but i can't find where it is declared in the surrounding scope?

2 Likes

What if we said that is just doesn't support bindings -- go back to if let for that. But you could use opt is Some(_) just the same as opt.is_some(), without needing such dedicated methods for the task.

2 Likes

Then it is no different from matches!(opt, Some(_)) (aside syntax)...

1 Like

It wouldn't be the first time a language feature deprecated a library macro.

3 Likes

IIRC the other times a language feature replaced a macro (? and async/await being the ones I'm familiar with), there were at least some corner cases that the language feature handled better than any macro ever could, especially at scale when several uses of the feature were nested across several crates.

But is and matches! would be exactly identical in behavior, if I understand the current proposals correctly, so adding an is keyword for this seems like a wash to me.

4 Likes

Sounds like it'd be nice to have a clippy lint that proposes to use matches! instead of if let <pat> = <expr> if the pattern has no bindings... at least I feel like that will always strictly improve readability. let is very deeply connected with the idea of binding a name, at least in my brain. I always startle when I see things like if let <const> = <expr>.

3 Likes

It's a Rusty Yoda condition! :slightly_smiling_face:

5 Likes

Yoda must have hit his head, though. if let 42 = x {} "if let forty-two equals x"

2 Likes

Although it's kind of silly, you can even match on values in function arguments, if the values are irrefutable:

enum Foo {
    TheOnlyVariant,
}

fn foo(Foo::TheOnlyVariant: Foo, (): ()) {}
1 Like