12.6 while let: Looping While a Pattern Matches
Right, so you’ve met if let, the charmingly concise syntax that lets you ditch the clunky match when you only care about one arm. while let is its slightly more obsessive cousin. It does exactly what it says on the tin: it loops while a pattern continues to let itself be matched.
Think of it as a while loop that’s also a pattern-matching ninja. Instead of a simple boolean condition, you give it a pattern. The loop keeps running its body for as long as the value on the right side of the = happily fits into the pattern on the left.
Here’s the classic example, and it’s a classic for a reason because it’s everywhere. Let’s talk about popping things off a stack.
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
// Here's the magic
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
So, what’s happening here? The stack.pop() method returns an Option<T>. It returns Some(value) if there was something to pop, and None if the stack was empty. The while let construct takes that returned Option and tries to fit it into the pattern Some(top).
- On the first iteration:
pop()returnsSome(3). PatternSome(top)matches!topis bound to3, the body runs, and we print “Popped: 3”. - On the second:
pop()returnsSome(2). Matches. Print “Popped: 2”. - On the third:
pop()returnsSome(1). Matches. Print “Popped: 1”. - On the fourth:
pop()returnsNone. The patternSome(top)says, “I only work withSomevariants, thank you very much.” It fails to match. The loop immediately exits.
We just gracefully drained the entire vector without a single explicit match or if let inside the loop condition. Elegant, isn’t it?
Why Not Just Use loop + match?
You absolutely could. The code above is essentially sugar for this more verbose version:
loop {
match stack.pop() {
Some(top) => {
println!("Popped: {}", top);
}
None => {
break;
}
}
}
The while let version is objectively cleaner. It compresses the four lines of match ceremony into a single, clear line. It reduces visual noise and lets you focus on the logic that actually matters: “while there’s a value, do this with it.” It’s a perfect example of Rust’s philosophy: giving you the power of exhaustive pattern matching but providing ergonomic shortcuts for the common cases so you don’t go mad.
It’s Not Just for Option<T>
Don’t pigeonhole while let into just working with options. It works with any enum! Imagine you’re writing a parser or walking a tree structure. Let’s say you have a simplistic list of operations:
#[derive(Debug)]
enum Operation {
Add(i32),
Subtract(i32),
Done,
}
let mut op_list = vec![
Operation::Add(10),
Operation::Subtract(5),
Operation::Done,
];
let mut total = 0;
while let Some(operation) = op_list.pop() {
match operation {
Operation::Add(x) => total += x,
Operation::Subtract(x) => total -= x,
Operation::Done => break, // We have to handle this here
}
println!("Current total: {}", total);
}
Wait, that’s a bit clunky. We’re using while let to get the Option from pop(), but then we have to do another match inside the loop to handle the actual Operation enum. This is where you can get clever and nest patterns. Behold:
let mut total = 0;
while let Some(operation) = op_list.pop() {
if let Operation::Done = operation {
break;
}
// ... now handle Add and Subtract
}
Better, but still not ideal. The real power move is to combine the patterns. You can use while let with any pattern, not just a single variant.
let mut total = 0;
while let Some(operation @ (Operation::Add(_) | Operation::Subtract(_))) = op_list.pop() {
match operation {
Operation::Add(x) => total += x,
Operation::Subtract(x) => total -= x,
Operation::Done => unreachable!(), // We've already filtered this out!
}
println!("Current total: {}", total);
}
Now that’s expressive. The loop itself says: “Keep going while there’s an operation and that operation is either Add or Subtract.” The Done variant is filtered out by the loop condition itself, so the body only contains logic for the variants we actually care about. This is how you level up from just using syntax to truly wielding it.
The Gotcha: Ownership and Borrowing
This is Rust, so we can’t escape the ownership police. The pattern in a while let follows the same rules as any other pattern. This means it will move or borrow values just like a match or if let would.
let mut things = vec![String::from("A"), String::from("B"), String::from("C")];
// This works, but it's a borrow. The strings stay in the vector.
while let Some(thing) = things.get(0) { // .get() returns Option<&String>
println!("First thing is: {}", thing);
things.remove(0); // We modify the vec, but the borrow from .get() is over
}
// This consumes the vector! It moves each String out.
while let Some(thing) = things.pop() { // .pop() returns Option<String>
println!("Consumed: {}", thing);
}
// 'things' is now empty and can't be used again
Always be conscious of whether your method returns an Option<T> (moving ownership) or an Option<&T> (borrowing). The while let doesn’t change the rules; it just applies them repeatedly until the pattern breaks.