Pattern Matching Mastery 🎯
You met match in Lesson 9 and have leaned on
it ever since. Time to see the whole toolbox: patterns are a small language
hiding inside Rust, and fluency in it is what makes code read like prose.
Patterns are everywhere, not just in match 📍
Surprise: every let you have ever written was a pattern.
let x = 5 matches the value against the pattern x,
which happens to always succeed. But patterns can take things apart:
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 3, y: 7 };
let Point { x, y } = p; // one line, both fields out
println!("x={x} y={y}");
let (name, score) = ("rusty", 98);
println!("{name} scored {score}");
}
This is called destructuring: the pattern mirrors the shape
of the data, and the pieces fall out into variables. Function parameters can
do it too: fn show((x, y): (i32, i32)) is a pattern in disguise.
Ranges, ors, and guards 🎚️
fn main() {
for n in [0, 3, 7, 30, 42, 99] {
let label = match n {
0 => "zero",
1..=9 => "single digit",
10 | 20 | 30 => "a round number we like",
x if x % 2 == 0 => "even",
_ => "odd",
};
println!("{n}: {label}");
}
}
Three new tricks in one match. 1..=9 matches a whole
range. 10 | 20 | 30 is an
or-pattern: any of these. And x if x % 2 == 0
is a guard: the pattern grabs the value, then the
if gets a vote. Arms are checked top to bottom, first winner
takes it, which is why 30 says "round" and not "even".
Naming what you match: the @ binding 🏷️
A range pattern matches but forgets the actual value. The @
sign lets you test and keep it, and patterns nest as deep as your
data does:
enum Shape {
Circle { radius: u32 },
Rect { w: u32, h: u32 },
}
fn main() {
let shapes = [
Shape::Circle { radius: 2 },
Shape::Circle { radius: 40 },
Shape::Rect { w: 5, h: 5 },
Shape::Rect { w: 3, h: 8 },
];
for shape in &shapes {
match shape {
Shape::Circle { radius: r @ 1..=3 } => println!("small circle, radius {r}"),
Shape::Circle { radius } => println!("big circle, radius {radius}"),
Shape::Rect { w, h } if w == h => println!("square, {w} by {h}"),
Shape::Rect { w, h } => println!("rectangle, {w} by {h}"),
}
}
}
Read the first arm out loud: "a Circle whose radius is 1 to 3, and call that
radius r." The compiler still checks you covered every shape.
That guarantee, exhaustiveness, is the quiet superpower:
add a Triangle variant next month and every match in the
project that forgot about it becomes a compile error, not a 3am bug.
The one-pattern family: if let, let else, while let 👨👩👧
A full match is overkill when you care about one shape.
Rust has three shortcuts, and you will meet all of them in real code:
fn first_word(s: &str) -> Option<&str> {
s.split_whitespace().next()
}
fn main() {
let input = "hello world";
if let Some(word) = first_word(input) {
println!("first word: {word}");
}
let Some(word) = first_word(input) else {
println!("no words at all!");
return;
};
println!("definitely have: {word}");
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("popped {top}");
}
}
if let: "if it fits this pattern, do this."
let else: "it must fit, or we bail out right here," which keeps
the happy path unindented. while let: "keep going as long as it
fits," the idiomatic way to drain anything. All three are just
match wearing comfortable clothes.
matches!: a pattern as a yes/no question ✅
fn main() {
let ch = 'k';
println!("lowercase letter? {}", matches!(ch, 'a'..='z'));
let maybe: Option<i32> = Some(4);
if matches!(maybe, Some(n) if n % 2 == 0) {
println!("an even number is hiding in there");
}
}
matches! turns any pattern, guards included, into a
bool. Perfect inside filter calls and
if conditions where a full match would be noise.
The tiny text adventure
Write fn respond(cmd: &str) -> String that understands
"go <direction>" (north/south/east/west only),
"take <item>", and "quit", with friendly
replies for everything else. Use split_once(' ') and one
match with or-patterns, an @ binding, and a guard.
Feed it a few commands from main.
Reveal solution
fn respond(cmd: &str) -> String {
match cmd.trim().split_once(' ') {
None if cmd.trim() == "quit" => "bye! 👋".to_string(),
None => format!("just '{}'? I need a verb and a noun", cmd.trim()),
Some(("go", dir @ ("north" | "south" | "east" | "west"))) => {
format!("you march {dir}")
}
Some(("go", other)) => format!("'{other}' is not a direction"),
Some(("take", item)) => format!("you take the {item}"),
Some((verb, _)) => format!("I do not know how to '{verb}'"),
}
}
fn main() {
for cmd in ["go north", "go up", "take lamp", "dance wildly", "quit"] {
println!("> {cmd}");
println!("{}", respond(cmd));
}
}
One match, six behaviors, no nested ifs, and the compiler guarantees no command falls through unanswered. This shape, "parse a little, match a lot," is the backbone of every command-line tool and network protocol parser ever written in Rust. 🗺️
matches!
for a yes/no, if let for one case, let else for
"must be", while let for draining, full match when
every case deserves an answer. Your future reviewers will know exactly what
you meant at a glance.