Concise Control Flow with if let and let else
In this chapter, we'll explore handle patterns concisely with if let and let else syntax.
Concise Control Flow with if let and let else
The if let syntax lets you combine if and let to handle values that match one pattern while ignoring the rest. Consider this code that matches on an Option<u8>:
let config_max = Some(3u8)
match config_max {
Some(max) => println("The maximum is configured to be ", max),
_ => (),
}We only care about the Some case. That's a lot of boilerplate for one case!
Using if let
Instead, write this more concisely with if let:
let config_max = Some(3u8)
if let Some(max) = config_max {
println("The maximum is configured to be ", max)
}if let takes a pattern and an expression separated by =. It works the same as a match where the expression is given to the match and the pattern is its first arm.
The code in the if let block runs only if the value matches the pattern. Using if let means less typing, less indentation, and less boilerplate. However, you lose the exhaustive checking that match enforces.
if let with else
You can include an else with if let:
let config_max = Some(3u8)
if let Some(max) = config_max {
println("The maximum is configured to be ", max)
} else {
println("No maximum configured")
}This is equivalent to:
match config_max {
Some(max) => println("The maximum is configured to be ", max),
_ => println("No maximum configured"),
}Chaining if let
Chain multiple if let expressions:
enum Color {
Rgb(u8, u8, u8),
Hsl(u8, u8, u8),
Named(string),
}
fn describe_color(color: Color) -> void {
if let Color::Rgb(r, g, b) = color {
println("RGB color: ", r, ", ", g, ", ", b)
} else if let Color::Hsl(h, s, l) = color {
println("HSL color: ", h, ", ", s, ", ", l)
} else if let Color::Named(name) = color {
println("Named color: ", name)
}
}let else
When you need a value from a pattern or want to return/break early, use let else:
fn get_count_item(s: string) -> (u64, string) {
let mut iter = s.split_whitespace()
let count_str = iter.next()
let item = iter.next()
let Some(count_str) = count_str else {
panic("Can't segment count item")
}
let Ok(count) = count_str.parse() else {
panic("Can't parse integer")
}
let Some(item) = item else {
panic("Can't segment item")
}
return (count, item)
}The let else pattern binds variables when the pattern matches. If it doesn't match, the else block must diverge (return, break, continue, or panic).
Compare this to the equivalent with match:
let count_str = match iter.next() {
Some(s) => s,
None => panic("Can't segment count item"),
}let else is more concise when you need the value or want to handle the error case with an early exit.
while let
Similar to if let, while let loops while a pattern matches:
let mut stack: Vec<int> = [1, 2, 3]
while let Some(top) = stack.pop() {
println(top)
}This prints 3, 2, 1. The loop continues as long as pop() returns Some(value). When it returns None, the loop ends.
Choosing Between match and if let
Use match when:
- You need to handle all variants of an enum
- Exhaustive checking is important for correctness
- You want the compiler to catch missing cases
Use if let when:
- You only care about one or two cases
- You want less verbose code
- The ignored cases can safely be ignored
Use let else when:
- You need to extract a value from a pattern
- You want to exit early if the pattern doesn't match
- You're dealing with multiple fallible operations
Summary
We've covered how to use enums to create custom types that can be one of a set of enumerated values. The standard library's Option<T> type helps you use the type system to prevent errors. When enum values have data inside, you can use match or if let to extract and use those values.
Your Mana programs can now express concepts in your domain using structs and enums. Creating custom types ensures the compiler catches type mismatches at compile time rather than runtime.
Next, let's look at Mana's module system in Chapter 7.