if let


<- back | tags: /dev/journal rust

I've been working with rust for a little while (read: a year-ish). Generally, I'm a proponent of the language and typically enjoy working with it in both my career and on personal side projects. That said, I'm also a slow learner. I seem unable to grok rust's if let syntax.

Whenever the opportunity to use it presents itself I just continue doing the same verbose match syntax. It's a rather silly predicament. Whenever it's pointed out that I could refactor to utilize if let then I invariably realize that I indeed could. Why it takes someone pointing it out to me at this point in my life is unknown.

At the recommendation of my friend sleepysticks I've decided to take this particular gap a bit more seriously and write about it. Hopefully, it'll solidify if I work through my thoughts, right?

What is it? Concise control flow

Summarily, if let is syntactic sugar that combines if and let into a concise package.

The if let syntax lets you combine if and let into a less verbose way to handle values that match one pattern while ignoring the rest.

Thanks Rust Programming Book. I have been told repeatedly that it's useful when we only care about one match arm. That's totally straight forward, right? It's a short-hand for match where we only care about a single pattern.

Example patterns

I was thinking, perhaps, I'm missing these patterns because I end up seeing slight variations. Still where we only care about a single match arm but variations nonetheless. So, let's go through some examples to get this concept to stick.

For the sake of being thorough I'm going to write verbosely. As a matter of preference, I like it when things are extensively written out so I can reason my way through it. I find it especially helpful when I'm either trying to memorize something or if I'm learning a new concept. All that said - you should be able to copy and paste the following snippets into somewhere like the Rust Playground and they should work given you aren't from the future and the language hasn't drastically changed syntax.

Option

Here, we only care about an Options if it's something:

/// Demonstrate `match` syntax with an `Option`
fn main() {
    let x: i32 = 3;

    match foo(x) {
        Some(a) => println!("{}", a),
        _ => ()
    }
}

/// Wrap some i32 in `Some()`
fn foo(x: i32) -> Option<i32> {
    Some(x)
}

Which can be refactored into if let syntax like so:

/// Demonstrate `if let` syntax with an `Option`
fn main() {
    let x: i32 = 3;

    if let Some(x) = foo(x) {
        println!("{}", x);
    }
}

/// Wrap some i32 in `Some()`
fn foo(x: i32) -> Option<i32> {
    Some(x)
}

OK Result

Similarly, we can apply this shortcut to Results:

/// Demonstrate `match` syntax with an `Ok()`
fn main() {
    let x: i32 = 3;

    match foo(x) {
        Ok(a) => println!("{}", a),
        _ => ()
    }
}

/// Wrap some i32 in `Ok()`
fn foo(x: i32) -> Result<i32, &'static str> {
    Ok(x)
}

Refactored with if let:

/// Demonstrate `if let` syntax with an `Ok()`
fn main() {
    let x: i32 = 3;

    if let Ok(x) = foo(x) {
        println!("{}", x);
    }
}

/// Wrap some i32 in `Ok()`
fn foo(x: i32) -> Result<i32, &'static str> {
    Ok(x)
}

Err Result

Maybe there are times that we just want to check if something is an Err() and do something special if it is. We can use the pattern here too!

/// Demonstrate `match` syntax with an `Err()`
fn main() {
    let x: i32 = 3;

    match foo(x) {
        Err(e) => println!("{}", e),
        _ => ()
    }
}

/// Return an `Err()`
fn foo(x: i32) -> Result<i32, &'static str> {
    Err("That didn't work by design")
}

Refactored into if let:

/// Demonstrate `if let` syntax with an `Err()`
fn main() {
    let x: i32 = 3;

    if let Err(x) = foo(x) {
        println!("{}", x);
    }
}

/// Return an `Err()`
fn foo(x: i32) -> Result<i32, &'static str> {
    Err("That didn't work by design")
}

The trivial examples make sense. They're also incredibly similar to one another but I think they get the gist across.

Thoughts

I've been staring at these if let examples for a while trying to deduce what my hang up is. I think that I'm not convinced the point of the expression makes sense as a shortcut for a one-arm match. By that I mean that the way I'm reading it out loud isn't an easy translation to what's happening for me.

The way I read it at first glance makes me think it's letting something be a Result<T> or an Option<T> that still needs an unwrap() - which isn't true considering how we consume T inside of the if let {}.

if let Ok(x) = foo(x) { // we can just use `x` in here }

I can reason that, after a pinch of mental gymnastics on my part, what's actually happening here is more like:

If foo(x) returns an Ok(x) then let x be the unwrapped contents of that Result. Which, I guess isn't much different than a match arm in that it's unwrapping things for you so you can just use whatever it is after the =>.

Fine. That makes sense.