Skip to content
AgileLogics
7 min read

Pattern Matching in Depth

Prerequisites: Iterators

Level 1 used match on simple enums. Patterns can do much more: pull apart structs, tuples and nested enums, test ranges, add extra conditions, and bind names to the parts you care about. Patterns appear in match, if let, while let, let, for, and function parameters.

Syntax

fn main() {
    let value = 7;
    let text = match value {
        0 => "zero",                       // literal
        1 | 2 => "one or two",             // several alternatives
        3..=9 => "a single digit",         // inclusive range
        n if n < 0 => "negative",          // match guard: extra condition
        _ => "big",                        // wildcard: anything else
    };
    println!("{text}");

    let point = (3, -2);
    let (x, _) = point;                    // destructure a tuple, ignore a part
    let is_small = matches!(value, 0..=9); // true/false instead of a full match
    println!("{x} {is_small}");
}
Pattern Meaning
Point { x, y } destructure a struct into variables x and y
Point { x: 0, .. } x must be 0; ignore the other fields
(first, .., last) first and last items of a tuple
[first, rest @ ..] first element of a slice and a sub-slice of the rest
id @ 1..=99 test a pattern and bind the matched value to id
Some(n) if n > 5 a guard: the arm matches only if the condition is true

match must be exhaustive: every possible value has to be covered by some arm. That is what makes adding a new enum variant safe — the compiler lists every match that needs updating.

let-else

let PATTERN = value else { ... }; binds the pattern if it matches, and otherwise runs the else block, which must leave the current scope (with return, break, continue or a panic). It keeps the "happy path" unindented:

fn parse_pair(text: &str) -> Option<(i32, i32)> {
    let Some((a, b)) = text.split_once(',') else {
        return None;
    };
    let (Ok(a), Ok(b)) = (a.trim().parse(), b.trim().parse()) else {
        return None;
    };
    Some((a, b))
}

Example

#[derive(Debug)]
enum Shape {
    Circle { radius: f64 },
    Rect { w: f64, h: f64 },
    Triangle(f64, f64, f64),
}

enum Message {
    Move { x: i32, y: i32 },
    Write(String),
    Paint(Shape),
    Quit,
}

fn describe(msg: &Message) -> String {
    match msg {
        Message::Move { x: 0, y: 0 } => String::from("stay put"),
        Message::Move { x, y: 0 } => format!("move horizontally by {x}"),
        Message::Move { x, y } => format!("move to ({x}, {y})"),
        Message::Write(text) if text.is_empty() => String::from("write nothing"),
        Message::Write(text) => format!("write '{text}'"),
        Message::Paint(Shape::Circle { radius }) => format!("paint a circle, r = {radius}"),
        Message::Paint(Shape::Rect { w, h }) if w == h => format!("paint a {w}x{h} square"),
        Message::Paint(shape @ Shape::Rect { .. }) => format!("paint {shape:?}"),
        Message::Paint(Shape::Triangle(a, b, c)) => {
            format!("paint a triangle, perimeter {}", a + b + c)
        }
        Message::Quit => String::from("quit"),
    }
}

fn classify(numbers: &[i32]) -> String {
    match numbers {
        [] => String::from("empty"),
        [only] => format!("just {only}"),
        [first, .., last] if first == last => format!("starts and ends with {first}"),
        [first, rest @ ..] => format!("{first} then {} more", rest.len()),
    }
}

fn age_group(age: u32) -> String {
    match age {
        0 => String::from("newborn"),
        child @ 1..=12 => format!("child aged {child}"),
        teen @ 13..=19 => format!("teenager aged {teen}"),
        adult => format!("adult aged {adult}"),
    }
}

fn main() {
    let messages = [
        Message::Move { x: 0, y: 0 },
        Message::Move { x: 5, y: 0 },
        Message::Move { x: 1, y: 2 },
        Message::Write(String::new()),
        Message::Write(String::from("hello")),
        Message::Paint(Shape::Circle { radius: 1.5 }),
        Message::Paint(Shape::Rect { w: 2.0, h: 2.0 }),
        Message::Paint(Shape::Rect { w: 2.0, h: 3.0 }),
        Message::Paint(Shape::Triangle(3.0, 4.0, 5.0)),
        Message::Quit,
    ];
    for msg in &messages {
        println!("{}", describe(msg));
    }

    println!("{}", classify(&[]));
    println!("{}", classify(&[7]));
    println!("{}", classify(&[4, 1, 4]));
    println!("{}", classify(&[1, 2, 3, 4]));

    for age in [0, 8, 16, 40] {
        println!("{}", age_group(age));
    }

    let quits = messages.iter().filter(|m| matches!(m, Message::Quit)).count();
    println!("quit messages: {quits}");
}

Expected output

stay put
move horizontally by 5
move to (1, 2)
write nothing
write 'hello'
paint a circle, r = 1.5
paint a 2x2 square
paint Rect { w: 2.0, h: 3.0 }
paint a triangle, perimeter 12
quit
empty
just 7
starts and ends with 4
1 then 3 more
newborn
child aged 8
teenager aged 16
adult aged 40
quit messages: 1

Arms are tried top to bottom, so more specific patterns must come before general ones: Move { x: 0, y: 0 } has to be listed before Move { x, y }.

Common mistakes

  • Leaving a case out. Removing the Message::Quit arm gives error[E0004]: non-exhaustive patterns: `&Message::Quit` not covered. Add the arm (or _ if you really mean "anything else").
  • Putting a catch-all arm first. Later arms become unreachable and the compiler warns unreachable pattern.
  • Forgetting that a guard doesn't count toward exhaustiveness. The compiler can't prove n if n >= 0 and n if n < 0 cover everything, so a final _ arm is still required.
  • Writing a let-else whose else block doesn't leave the scope: error[E0308]: `else` clause of `let...else` does not diverge.
  • Expecting a variable name in a pattern to compare against an existing variable. match x { limit => ... } doesn't compare with a limit variable in scope — it creates a new binding that matches everything. Use a guard: n if n == limit.

Practice

Write fn fizzbuzz(n: u32) -> String using a single match on the tuple (n % 3, n % 5), and print the results for 1 through 15 on one line, separated by spaces.

Show solution
fn fizzbuzz(n: u32) -> String {
    match (n % 3, n % 5) {
        (0, 0) => String::from("FizzBuzz"),
        (0, _) => String::from("Fizz"),
        (_, 0) => String::from("Buzz"),
        _ => n.to_string(),
    }
}

fn main() {
    let line: Vec<String> = (1..=15).map(fizzbuzz).collect();
    println!("{}", line.join(" "));
}
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz