I have a hard time understanding the "if let" syntax

I was reading through the book, and stumbled upon chapter 6.3: Concise Control Flow with if let

I read the part about "if let" seven times or so, but I can still not wrap my head around why this syntax is like that.
I feel that I can probably apply the syntax to have result, but that unless I understand why it is the way it is, I'll never be able to memorise and integrate the concept in my brain.

Instead of

if let Some(3) = some_u8_value {
    println!("three");
}

I guess I would expect something like

if some_u8_value <matches> Some(3) {
    println!("three");
}

or

if Some(3) <matches> some_u8_value {
    println!("three");
}

where <matches> could be any keyword, really.

I have been doing some more reading, and then found the chapter 18.1: All the Places Patterns Can Be Used

I have to say, it did not help me understand at all why the if let syntax is structured the way it is.

Did anyone also struggle with it, and found either a simple explanation, or some article "if let syntax for dummy" that helped you wrap your head around the concept?

Thanks!

1 Like

Sounds like you understand it just fine, you just think the syntax should be different?

1 Like

@jethrogb no, I really don't understand it.
It totally confuses me.

The pseudo syntax I wrote was just me trying to clarify what I would have expected for a short hand simple matching.
It's probably worth mentioning that I'm a total noob in rust, and come from a Kotlin universe.

I've been meaning to write a blog post on this for a while, but haven't got round to it yet, so I'll give the condensed version here :slight_smile:

To understand why the if let syntax is the way it is, we should first look at the let syntax. In most cases, you'll see it used for simple variable declaration, like this:

let x = 123;

What might not be apparent at first glance is that the left side of a let isn't simply a name - it's a pattern, and accepts the exact same syntax as a match arm! There is however, an important restriction - you can only specify patterns that are 'irrefutable'. In simpler terms, this means that you can only use a pattern which the compiler knows will match for every possible value of the type that you're matching against.

For example, this is allowed:

enum Example {
    Data(i32),
}

let x = Example::Data(123); // wrap the data
let Example::Data(y) = x;   // unwrap the data via a pattern

dbg!(y); // prints 'y = 123'

This works, because the compiler understands that Example::Data(y) is a pattern that will match for every possible value of type Example.

However, this is no longer true if we add an extra variant, and so you get an error:

enum Example {
    DataA(i32),
    DataB(f32),
}
    
let x = Example::DataA(123);
let Example::DataA(y) = x; // error[E0005]: refutable pattern in local binding: `DataB(_)` not covered

So if we can't use let for this pattern, what can we use? This is where if let comes in:

enum Example {
    DataA(i32),
    DataB(f32),
}

let x = Example::DataA(123);
if let Example::DataA(y) = x {
    dbg!(y); // you can use 'y', but only inside the block
}

Notice how the syntax for if let and let is almost exactly the same! This symmetry is why that syntax was picked - if let is a natural extension of the existing let syntax.

31 Likes

Looks like you are confused about pattern matching in general. Patterns are not expressions, and you shouldn't expect them to behave like so. The pattern syntax is designed for extracting parts of the matching value, it just "happens to work" with inline literals too (as in: it works because it's consistent this way).

You can say if foo == Some(3) instead of if let Some(3) = foo, but the latter is more powerful if you don't know the exact value and want to extract it as if let Some(x) = foo.

3 Likes

Your example blew my mind a bit!

The fact that let Example::Data(y) = x; works is still not clear to me, but at last by accepting that, the if let syntax makes a lot more sense.

Ultimately, @H2CO3 is right, I'm lacking understanding in patterns.

1 Like

I mean Option is basically

enum Option<T>{
    Some(T),
    None,
}

so it works the same way as Example::DataA and Example::DataB variants. Maybe playing with those examples would help you understand it.

1 Like

On the particular syntax choice of if let, I believe this was also inspired by Swift's optional binding:

@H2CO3, @17cupsofcoffee I have it now!
What was actually really the problem for me was to understand that in let number = 32;, number is a pattern, where number is the binding.

While in let Example::Data(y) = x;, Example::Data(y) is the pattern, and y is the binding.
Now that I get that (thanks to @H2CO3's article), it makes sense :slight_smile:

4 Likes

Maybe an analogy would help?

Imagine you have a hard metal sheet with two holes, a circle-shaped and a square-shaped one. There are little instruments in both. The instrument in the circle-shaped hole detects the color of any object put through the hole, while the instrument in the square-shaped hole measures the temperature of the object.

Now imagine you are trying to put a sphere or a cube through the holes. If you have a sphere, it will only go through the circular hole. In this case, you'll be able to ask the instrument what color it is. If you are holding a cube, you can only fit it into the square hole, in which case you'll know its temperature.

The object in your hand corresponds to the RHS of the if let, or the scrutinee of a match expression. The holes in the sheet correspond to the patterns you are matching against. The instruments and their output corresponds to accessing the destructured inner parts of the successfully pattern-matched value.

2 Likes

Pattern matching is essentially destructuring with extra conditions.

Are you maybe familiar with (modern) Javascript? Destructuring looks like this:

let x = {data: 123};
let {data: y} = x; // Destructuring
console.log(y); // prints: 123

The corresponding Rust code

let x = Example::DataA(123);
let Example::DataA(y) = x;
dbg!(y);

basically just adds a condition: The destructuring only works if the right side is of the type Example::DataA, otherwise it fails. The Rust compiler allows possible failure only if it is handled, that's the purpose of the if - it deals with failed destructuring.

1 Like

It would probably help if you can get a deeper understanding of pattern matching and how that works exactly first. Then, I think 'if let' would probably make more sense. Do some searching for pattern matching in elixir and if you can understand how it works there I think it will help you grasp the idea 'if let' in Rust. '=' sign in elixir is not thought of as assignment but actually referred to as the match operator.

Anyways, I'm also new to Rust but, coming from elixir and understanding how match works there has really made it easy to understand in rust. It is not entirely the same but very similar in a lot of ways.

Here's a link to get you started: https://elixir-lang.org/getting-started/pattern-matching.html

2 Likes

I love the example showing how you proceed from let to if let. In the beginning I didn't know it was essentially a match statement and it took a while for it to gel. Eventually I got it to gel just by imagining that let was get, turning it into "if you get A when you do B". Then a few months later I came across a post demonstrating how it was an irrefutable match statement and I was finally able to explain it to others if I had to (instead of just saying "yeah, it's kind of weird, just imagine that it means...").

1 Like

It's only when I figured out that we are destructuring in the let Example::DataA(y) portion that it began to make sense to me.

It is true though that this feels like you have to think backwards from the problem in order to understand why it is the way it is. I too was exactly in the same position as you when I first had to wrap my head around this bit of syntax.

Curious if people do believe, with hindsight, that if let is the best syntax for this.

It might not be the best one, but it's a (destructuring) let combined with an if condition. I can't think of a better syntax.

Now that it makes sense to me, I guess that the key to understand it is to really understand that with let something = something_else, you are not doing assignment but something else (called binding) apparently.

And then understanding that something is a pattern, even if you expression is let x = 10.

But to understand that, I first had to understand that

let (x) = (10) worked, let (x, y) = (10, 42) was a thing, then

struct MyStruct { foo: u8, bar: u8 }

let MyStruct { foo: x, bar: y } = MyStruct { foo: 10, bar: 42 };

was a thing.

So, yeah, patterns, from simple to more complex.

It's a thing of beauty, and I never encountered that before (I am coming from Android development, so a lot of java and kotlin)

Once that made sense... then if let did to :slight_smile:

1 Like

Uh, doesn't Kotlin have destructuring?

You can destructure data classes to tuples, but that is about it.
So it's not on the same league!

I would argue that the destructing of data classes in kotlin is syntaxic sugar, not real pattern matching.
Nothing wrong with syntaxic sugar, but it means that there is no "deep concept" behind it that you need to understand before using it properly, as opposed to pattern matching like in rust.

But destructuring and matching is the dual problem to constructing expressions. Of course you have to think backwards. The syntax would be inconsistent if you didn't have to.

1 Like