Step 5 of 7
if let and let...else
match handles every case. Often you only care about one pattern, and match gets wordy:
match find_price(&menu, "cake") {
Some(price) => println!("cake costs {}", price),
None => {}
}
if let
if let runs a block only when a value matches one pattern, binding the data on the way:
fn main() {
let discount: Option<u32> = Some(15);
if let Some(percent) = discount {
println!("{}% off today", percent);
}
let coupon: Option<u32> = None;
if let Some(percent) = coupon {
println!("coupon: {}% off", percent);
} else {
println!("no coupon");
}
}
15% off today
no coupon
Read if let Some(percent) = discount as "if discount matches Some(percent), then...". It works with any enum, not just Option: if let Shape::Circle(r) = shape { ... }. The trade-off: unlike match, if let doesn't check that you handled everything. That's what you want when the other cases really don't matter.
let...else
The opposite situation is common too: "get the value out, and if it isn't there, give up". let ... else binds the value for the rest of the enclosing block (the function body, or a loop body as below), and runs the else block when the pattern doesn't match. That block must diverge: leave with return, break, continue or a panic, because there's no value to continue with.
struct Item {
name: String,
price_cents: u32,
}
fn find_price(items: &[Item], name: &str) -> Option<u32> {
for item in items {
if item.name == name {
return Some(item.price_cents);
}
}
None
}
fn order_total(items: &[Item], order: &[&str]) -> Option<u32> {
let mut total = 0;
for name in order {
let Some(price) = find_price(items, name) else {
return None;
};
total += price;
}
Some(total)
}
fn main() {
let menu = [
Item { name: String::from("tea"), price_cents: 300 },
Item { name: String::from("cake"), price_cents: 450 },
];
println!("{:?}", order_total(&menu, &["tea", "cake", "tea"]));
println!("{:?}", order_total(&menu, &["tea", "pizza"]));
}
Some(1050)
None
This is the guard clause style teams love: deal with the failure case right away, then carry on with the happy path, unindented. Without let...else you'd nest a match inside the loop.
let...else works with any pattern that can fail, including the Ok(...) of parse you met in the input module:
fn double(text: &str) -> Option<i32> {
let Ok(n) = text.trim().parse::<i32>() else {
return None;
};
Some(n * 2)
}
fn main() {
println!("{:?} {:?}", double(" 21 "), double("twenty"));
}
Some(42) None
Common mistakes
- An
elseblock that doesn't leave:
fn main() {
let input = "42";
let Ok(n) = input.parse::<i32>() else {
println!("not a number");
};
println!("{}", n);
}
error[E0308]: `else` clause of `let...else` does not diverge
After println!, the program would continue without an n. Add a return; (or break, continue, panic!).
if letwhen you need the value afterwards: a variable bound byif letonly exists inside its block. If the code after it needs the value, uselet...else.
Your task
Write fn price_for(items: &[Item], name: &str, quantity: u32) -> Option<u32>. It returns the cost of buying quantity of the named item, or None if the item doesn't exist or there isn't enough stock. The helper find gives you the item's index. Use let...else for the lookup.
(Why does find return an index instead of a &Item? Returning a reference that could come from either of two reference parameters needs a lifetime annotation, which the lifetimes module teaches. An index sidesteps that.)
Challenge 2
Print a bill. For each name in order, use if let on find_price: print NAME: PRICE if it's on the menu, or NAME: not available if not. Add up the prices that were found and finish with total: N:
cake: 450
pizza: not available
tea: 300
total: 750
Challenge 3
Write fn parse_point(line: &str) -> Option<(i32, i32)> that reads two whole numbers separated by whitespace, like "3 -4", and returns Some((3, -4)). Return None if there are fewer than two words, more than two, or a word isn't a number.
How to get the words one at a time: let mut words = line.split_whitespace(); then each words.next() returns Some(word), or None when there are no more words. It's another Option, so let...else fits perfectly. For the third-word check, words.next().is_some() tells you whether there's an extra word.
Previous: Option: a value that might be missing Next: Methods on enums and matches!