Let Control Flow
Rust has a few control flow constructs which differ from other languages. They are used for pattern matching:
if let
expressionslet else
expressionswhile let
expressions
if let expressions
The if let expression lets you execute different code depending on whether a value matches a pattern:
use std::time::Duration;
fn sleep_for(secs: f32) {
if let Ok(duration) = Duration::try_from_secs_f32(secs) {
std::thread::sleep(duration);
println!("slept for {duration:?}");
}
}
fn main() {
sleep_for(-10.0);
sleep_for(0.8);
}
let else expressions
For the common case of matching a pattern and returning from the function, use let else. The “else” case must diverge (return
, break
, or panic - anything but falling off the end of the block).
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
if let Some(s) = maybe_string {
if let Some(first_byte_char) = s.chars().next() {
if let Some(digit) = first_byte_char.to_digit(16) {
Ok(digit)
} else {
return Err(String::from("not a hex digit"));
}
} else {
return Err(String::from("got empty string"));
}
} else {
return Err(String::from("got None"));
}
}
fn main() {
println!("result: {:?}", hex_or_die_trying(Some(String::from("foo"))));
}
Like with if let
, there is a while let variant which repeatedly tests a value against a pattern:
fn main() {
let mut name = String::from("Comprehensive Rust 🦀");
while let Some(c) = name.pop() {
println!("character: {c}");
}
// (There are more efficient ways to reverse a string!)
}
Here String::pop returns Some(c)
until the string is empty, after which it will return None
. The while let
lets us keep iterating through all items.
This slide should take about 10 minutes.
if-let
- Unlike
match
,if let
does not have to cover all branches. This can make it more concise thanmatch
. - A common usage is handling
Some
values when working withOption
. - Unlike
match
,if let
does not support guard clauses for pattern matching.
let-else
if-let
s can pile up, as shown. The let-else
construct supports flattening this nested code. Rewrite the awkward version for students, so they can see the transformation.
The rewritten version is:
#![allow(unused)]
fn main() {
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
let Some(s) = maybe_string else {
return Err(String::from("got None"));
};
let Some(first_byte_char) = s.chars().next() else {
return Err(String::from("got empty string"));
};
let Some(digit) = first_byte_char.to_digit(16) else {
return Err(String::from("not a hex digit"));
};
return Ok(digit);
}
}
while-let
- Point out that the
while let
loop will keep going as long as the value matches the pattern. - You could rewrite the
while let
loop as an infinite loop with an if statement that breaks when there is no value to unwrap forname.pop()
. Thewhile let
provides syntactic sugar for the above scenario.