DEV Community

Nicky Meuleman
Nicky Meuleman

Posted on • Originally published at nickymeuleman.netlify.app

Rust syntax: if let and while let

TL;DR:

  • if let is a fancy if condition.
  • while let is a fancy while loop.

They enter a codeblock if a destructuring assignment works.


With the if let syntax you combine an if condition with a destructuring let assignment.

Let's unpack (pun intended) that sentence.

The if condition

let my_pick: Option<u8> = Some(3);
if Some(3) == my_pick {
    println!("Honeybadger don't care.");
}
Enter fullscreen mode Exit fullscreen mode

This line prints, alerting everyone that Some(3) is equal to my_pick, and more importantly that honeybadgers don't care.

Destructuring assignments

If array destructuring works with square brackets, and tuple destructuring works via parentheses, it makes sense an Option could be destructured via Some.


A quick recap for array and tuple destructuring:

let arr = [1, 2, 3];
let [one, two, three] = arr;
println!("Counting: {}, {}, {}", one, two, three);
// Counting: 1, 2, 3

let tup = ("Daniel", "Ricciardo", 3);
let (first_name, last_name, racing_number) = tup;
println!("{} {} races with the number: {}", first_name, last_name, racing_number);
// Daniel Ricciardo races with the number: 3
Enter fullscreen mode Exit fullscreen mode

If you try to destructure what's inside my_pick to the number variable with Some(number), Rust will not compile and show a warning:

let my_pick: Option<u8> = Some(3);
let Some(number) = my_pick;
println!("Daniel Ricciardo, nicknamed 'the honeybadger' races with the number: {}", number);
// error
Enter fullscreen mode Exit fullscreen mode
error[E0005]: refutable pattern in local binding: `None` not covered
Enter fullscreen mode Exit fullscreen mode

Because my_pick has the type of Option<u8>, it will either be a Some(<u8>) or a None.
The piece of code above assumes there will always be some integer inside and neglects the possibility my_pick is None.

The Rust compiler won't let you do that, it requires every possibile value for the Option to be covered!

In this contrived example, humans can see my_pick will always be Some(3), but the compiler can't.

We can use a match to cover every possibility.

let my_pick: Option<u8> = Some(3);
let number = match my_pick {
    Some(num) => num,
    None => panic!("Oh no, no number!")
};
println!("Daniel Ricciardo, nicknamed 'the honeybadger' races with the number: {}", number);
Enter fullscreen mode Exit fullscreen mode

We could have also been even more specific and only execute the println! if the number inside the Option was 3, while ignoring any other possibility.

let my_pick: Option<u8> = Some(3);
let number = match my_pick {
    Some(3) => 3,
    _ => panic!("Oh no, no number!")
};
println!("Daniel Ricciardo, nicknamed 'the honeybadger' races with the number: {}", number);
Enter fullscreen mode Exit fullscreen mode

That's a bit verbose, we don't care about the None case and only want to do something if my_pick holds a Some.

Even worse, the way this code is written, it depends on a Some being there, else it will panic.
What we really want is to conditionally print that line.
If my_pick is a Some: print it, if it is None: ignore that println and continue with the program.

Combining an if condition with a destructuring attempt

Combining that destructuring attempt with an if condition gives us the if let syntax.

It tries to destructure, if that destructure works, the attached codeblock is entered.
If it doesn't work, the codeblock isn't entered and the code continues.

let my_pick: Option<u8> = Some(3);
if let Some(number) = my_pick {
    println!("Daniel Ricciardo, nicknamed 'the honeybadger' races with the number: {}", number);
}
Enter fullscreen mode Exit fullscreen mode

Using if let means less typing, less indentation, and less boilerplate code. However, you lose the exhaustive checking that match enforces

The Rust Programming Language book

In the case we only care about my_pick holding a Some(3):

let my_pick: Option<u8> = Some(3);
if let Some(3) = my_pick {
    println!("Daniel Ricciardo, nicknamed 'the honeybadger' races with the number: 3");
}
Enter fullscreen mode Exit fullscreen mode

The else case

As you would expect from an if condition, it can be combined with else if and else.
The else if condition can also be a conditional destructuring attempt with else if let.

let my_pick: Option<u8> = Some(33);
if let Some(3) = my_pick {
    println!("Daniel Ricciardo, nicknamed 'the honeybadger' races with the number: 3");
} else if let Some(33) = my_pick {
  println!("Max, Max, Max, super Max, Max");
} else {
  println!("Unknown driver");
}
Enter fullscreen mode Exit fullscreen mode

Here, my_pick is Some(33).
The if doesn't succeed anymore, the else if condition succeeds, printing a lyric from a Max Verstappen fansong.

If my_pick held yet another Some or if it held None, the else block would execute.

Do it again, and again

The while let syntax does almost the same thing.
Instead of continuing on when the end of the conditional codeblock is reached, it checks the condition again.
If the destructuring that is attempted there works again, enter the codeblock again! Rinse, repeat.

The pop method on a vector returns an Option.
The code below uses while let to print out the number if pop returns a Some with that number.
In the case where pop returns None, the codeblock will not be entered again and the loop stops.

let mut my_picks: Vec<u8> = vec![3, 33, 44];
while let Some(num) = my_picks.pop() {
    println!("{}", num);
}
Enter fullscreen mode Exit fullscreen mode

Like the regular while loop,
while let does not have an else if or an else case.


Not limited to the Option type

The if let and while let doesn't always need to try and destructure an Option.

The following piece of code tries to destructure an Ok(3) from a Result.

let my_pick: Result<u8, ()> = Ok(3);
if let Ok(3) = my_pick {
    println!("Daniel Ricciardo, nicknamed 'the honeybadger' races with the number: 3");
}
Enter fullscreen mode Exit fullscreen mode

The thing we try to destructure doesn't have to be the kind of enum variant that holds something, simple enums variants work too!

enum SesameStreetChar {
    Count,
    // others
}
let best_vampire = SesameStreetChar::Count;
if let SesameStreetChar::Count = best_vampire {
  println!("Ah, ah, ah!");
};
Enter fullscreen mode Exit fullscreen mode

In the previous example, a straight comparison would fail because the enum doesn't implement the PartialEq trait.

if SesameStreetChar::Count == best_vampire {
  println!("this does not compile");
}
// error
Enter fullscreen mode Exit fullscreen mode

if let still works.

if let SesameStreetChar::Count = best_vampire {
  println!("boo!");
}
Enter fullscreen mode Exit fullscreen mode

Discussion (0)