if let and while let are concise ways to apply pattern matching when we are only interested in a subset of cases.
In the previous article, we saw the power of match. Its exhaustiveness gives us incredible safety. Sometimes, being honest, it’s like “using a sledgehammer to crack a nut.”
There are situations where we only care about one variant of an Enum and want to ignore all the others. Writing a match block with a _ => () at the end works, but it adds a lot of visual noise (boilerplate).
For these cases, Rust offers us two “syntactic sugar” tools that clean up our code: if let and while let.
The problem of excessive match
Imagine we are working with a variable config that is an Option<String>. We only want to print a message if there is a value (Some). If it’s None, we don’t want to do anything.
With what we know so far, we would do this:
let config = Some(String::from("Dark Mode"));
match config {
Some(value) => println!("Configuration loaded: {}", value),
None => (), // Boilerplate: We have to write this obligatorily
}It’s a bit verbose, right? And also, the indentation eats up space.
The solution: if let
The if let syntax allows us to combine if and let to handle values that match a pattern and, in the process, ignore the rest.
The code above translates to:
let config = Some(String::from("Dark Mode"));
// Reads as: "If the pattern 'Some(value)' matches 'config', execute the block"
if let Some(value) = config {
println!("Configuration loaded: {}", value);
}Much cleaner!
Understanding the syntax
if let takes a pattern on the left and an expression on the right, separated by an equals sign =.
if let PATTERN = EXPRESSION {
// Code to execute if it matches
}Visual caution:
Although it uses the = sign, it is not a normal assignment. We are not storing the variable in the pattern; we are attempting Pattern Matching.
Adding else
Just like a normal if, if let can have an else block. This block will execute if the pattern does NOT match (that is, it would be equivalent to the _ case in match).
let coin = Coin::Euro;
if let Coin::Euro = coin {
println!("We have euros!");
} else {
println!("Not euros, we'll have to exchange currency.");
}Loops with while let
If if let is the concise version of a one-shot match, while let is the concise version of a loop that depends on a pattern.
It’s extremely useful when working with iterators or structures that can be “exhausted” (returning None at some point).
Imagine we have a vector and we want to keep popping elements until it’s empty. The pop() method returns Option<T> (Some if there is data, None if empty).
With loop and match (verbose):
let mut stack = vec![1, 2, 3];
loop {
match stack.pop() {
Some(value) => println!("{}", value),
None => break, // We have to manage the exit manually
}
}With while let (concise):
let mut stack = vec![1, 2, 3];
// Reads as: "While 'stack.pop()' returns 'Some(value)', execute the loop"
while let Some(value) = stack.pop() {
println!("{}", value);
}The loop runs as long as the pattern matches. As soon as pop() returns None, the pattern Some(value) no longer fits, and the loop ends automatically.
When to use which?
It’s easy to fall into the temptation of always using if let because it’s shorter, but losing the exhaustiveness of match has its risks.
Use match when… | Use if let when… |
|---|---|
| You need to handle multiple variants. | You only care about one specific variant. |
| You want the compiler to warn you if you add a new variant to the Enum (future-proofing). | You don’t mind ignoring all other variants. |
| The logic is complex and branching. | You want to execute simple code if something exists. |