Level 4 · Deep Cuts

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.

Exercise

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. 🗺️

🧠 The takeaway Reach for the smallest tool that says what you mean: 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.