Structs, Enums and Pattern Matching · about 45 min

Pattern Matching in Depth

Why this matters

You have used patterns since module 2 without calling them that: let (a, b) = pair;, match arms, for (i, x) in ...enumerate(). Patterns are a small language for describing the shape of a value and pulling it apart in one step. Used well, they replace chains of field accesses, index checks and ifs with code that reads like a description of the data. This lesson collects the pattern features you will use constantly, plus the three control-flow forms built on them: if let, while let and let else.

Destructuring structs, tuples and enums

struct Point {
    x: i32,
    y: i32,
}

enum Event {
    Click { pos: Point, button: u8 },
    Key(char),
    Resize(u32, u32),
}

fn main() {
    let p = Point { x: 3, y: -7 };
    let Point { x, y } = p;
    println!("x={x} y={y}");

    let events = [
        Event::Click { pos: Point { x: 10, y: 0 }, button: 1 },
        Event::Key('q'),
        Event::Resize(800, 600),
        Event::Click { pos: Point { x: 5, y: 5 }, button: 2 },
    ];
    for e in &events {
        match e {
            Event::Click { pos: Point { x, y: 0 }, .. } => println!("click on top edge at x={x}"),
            Event::Click { button: 2, .. } => println!("right click"),
            Event::Click { .. } => println!("other click"),
            Event::Key(c) => println!("key {c:?}"),
            Event::Resize(w, h) => println!("resize to {w}x{h}"),
        }
    }
}
x=3 y=-7
click on top edge at x=10
key 'q'
resize to 800x600
right click

Guards, ranges, alternatives and @

fn classify(n: i64) -> String {
    match n {
        i64::MIN..=-1 => String::from("negative"),
        0 => String::from("zero"),
        x @ (1 | 2 | 3 | 5 | 7) => format!("small prime {x}"),
        x @ 1..=9 => format!("digit {x}"),
        x if x % 2 == 0 => format!("big even {x}"),
        x => format!("big odd {x}"),
    }
}

fn main() {
    for n in [-4, 0, 5, 8, 42, 99] {
        println!("{}", classify(n));
    }
}
negative
zero
small prime 5
digit 8
big even 42
big odd 99

Binding modes: what do the bindings hold?

When you match on a reference (like &events in a for loop, or &self in a method), the bindings become references automatically: c in Event::Key(c) above is a &char. This is called default binding mode, and it is why matching a borrowed value never moves anything out of it. When you match on an owned value, bindings take the fields by value (moving non-Copy ones). You will see &x in patterns too: it dereferences, as in for &v in values.iter().

if let: one pattern, one branch

When you only care about one variant, a full match is noisy. if let runs a block only if a pattern matches:

fn main() {
    let words = ["apple", "x", "banana"];
    for w in words {
        if let Some(c) = w.chars().nth(1) {
            println!("{w}: second char {c}");
        } else {
            println!("{w}: too short");
        }
    }
    let config_port: Option<u16> = Some(8080);
    if let Some(port) = config_port {
        println!("listening on {port}");
    }
}
apple: second char p
x: too short
banana: second char a
listening on 8080

if let PATTERN = EXPR { ... } else { ... } is a match with one interesting arm and a catch-all. (Option, with its Some(value) and None variants, is the subject of the next module.)

while let: loop while a pattern matches

fn main() {
    let mut stack = vec![1, 2, 3];
    while let Some(top) = stack.pop() {
        println!("popped {top}");
    }
    println!("empty: {}", stack.is_empty());
}
popped 3
popped 2
popped 1
empty: true

The loop continues as long as stack.pop() returns Some, and stops at the first None. It is the natural way to drain a stack, a queue or any "get next thing until there is none" source.

let else: bind or bail out

Often a function must extract something or give up. let PATTERN = EXPR else { DIVERGE }; binds the pattern's variables for the rest of the block, or runs the else block, which must leave (return, break, continue or panic):

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

fn main() {
    for s in ["3, 4", "nope", "1,x"] {
        println!("{s:?} -> {:?}", parse_pair(s));
    }
}
"3, 4" -> Some((3, 4))
"nope" -> None
"1,x" -> None

Each step either succeeds and binds names at the top level, or exits immediately. The happy path stays unindented, unlike nested if lets. (parse returns Result, whose success variant is Ok; next module.)

matches! for yes/no questions

matches!(value, pattern) returns true if the pattern matches:

fn main() {
    let c = 'k';
    println!("{}", matches!(c, 'a'..='z' | 'A'..='Z'));
    let n = 15;
    println!("{}", matches!(n, x if x % 5 == 0));
}
true
true

Refutable and irrefutable patterns

A pattern that always matches, like (a, b) for a tuple or a plain name, is irrefutable. let and function parameters need irrefutable patterns. A pattern that can fail, like Some(x), is refutable and needs match, if let, while let or let else:

fn main() {
    let maybe: Option<i32> = Some(3);
    let Some(x) = maybe;
    println!("{x}");
}
error[E0005]: refutable pattern in local binding
 --> main.rs:3:9
  |
3 |     let Some(x) = maybe;
  |         ^^^^^^^ pattern `None` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
  = note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
  |
3 |     let Some(x) = maybe else { todo!() };
  |                         ++++++++++++++++

E0005, with the compiler offering the let else fix.

Common mistakes

Recap

Going further: the Rust Book chapter 19 ("Patterns and Matching") lists every pattern form, including slice patterns like [first, .., last] and [x, rest @ ..]. Try those on an array.

Exercises

Exercise 1 · write code

Destructure a point

Given struct Point { x: i32, y: i32 }, write fn locate(p: &Point) -> String using a single match with struct patterns and guards, returning:

The given main reads pairs x y from stdin and prints the result for each. For 0 0, 5 0, 0 -2, 3 4, -1 2, -1 -1, 2 -9:

origin
x-axis at 5
y-axis at -2
Q1
Q2
Q3
Q4

File: main.rs

Starting point

use std::io;

struct Point {
    x: i32,
    y: i32,
}

fn locate(p: &Point) -> String {
    String::new()
}

fn main() {
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let (a, b) = line.trim().split_once(' ').unwrap();
        let p = Point { x: a.parse().unwrap(), y: b.parse().unwrap() };
        println!("{}", locate(&p));
    }
}

Example

input

0 0
5 0
0 -2
3 4
-1 2
-1 -1
2 -9

expected output

origin
x-axis at 5
y-axis at -2
Q1
Q2
Q3
Q4
Hint 1

Point { x: 0, y: 0 } matches a literal; Point { x, y: 0 } binds x and requires y == 0.

Hint 2

Matching on &Point binds x and y as &i32; compare with *x > 0 in guards.

Reference solution
use std::io;

struct Point {
    x: i32,
    y: i32,
}

fn locate(p: &Point) -> String {
    match p {
        Point { x: 0, y: 0 } => String::from("origin"),
        Point { x, y: 0 } => format!("x-axis at {x}"),
        Point { x: 0, y } => format!("y-axis at {y}"),
        Point { x, y } if *x > 0 && *y > 0 => String::from("Q1"),
        Point { x, y } if *x < 0 && *y > 0 => String::from("Q2"),
        Point { x, .. } if *x < 0 => String::from("Q3"),
        _ => String::from("Q4"),
    }
}

fn main() {
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let (a, b) = line.trim().split_once(' ').unwrap();
        let p = Point { x: a.parse().unwrap(), y: b.parse().unwrap() };
        println!("{}", locate(&p));
    }
}

Exercise 2 · write code

Early exit with let-else

Write fn parse_move(line: &str) -> Option<(char, u32)> for commands like U 3, D 10, L 1, R 7: return Some((direction, steps)), or None if the line does not have exactly that shape (one of the letters U D L R, a space, a non-negative integer). Use let ... else { return None; } for each step.

The given main sums the moves read from stdin and prints the final position, skipping invalid lines with a message:

invalid: X 2
invalid: U
position: (2, 1)

for the input lines U 3, X 2, R 2, U, D 2.

File: main.rs

Starting point

use std::io;

fn parse_move(line: &str) -> Option<(char, u32)> {
    None
}

fn main() {
    let (mut x, mut y) = (0i64, 0i64);
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let Some((dir, n)) = parse_move(line.trim()) else {
            println!("invalid: {}", line.trim());
            continue;
        };
        let n = n as i64;
        match dir {
            'U' => y += n,
            'D' => y -= n,
            'L' => x -= n,
            _ => x += n,
        }
    }
    println!("position: ({x}, {y})");
}

Example

input

U 3
X 2
R 2
U
D 2

expected output

invalid: X 2
invalid: U
position: (2, 1)
Hint 1

let Some((d, n)) = line.split_once(' ') else { return None; };

Hint 2

parse returns a Result: let Ok(steps) = n.parse::<u32>() else { return None; };

Hint 3

Match the direction string against "U", "D", "L", "R" and return None for anything else.

Reference solution
use std::io;

fn parse_move(line: &str) -> Option<(char, u32)> {
    let Some((d, n)) = line.split_once(' ') else {
        return None;
    };
    let dir = match d {
        "U" => 'U',
        "D" => 'D',
        "L" => 'L',
        "R" => 'R',
        _ => return None,
    };
    let Ok(steps) = n.parse::<u32>() else {
        return None;
    };
    Some((dir, steps))
}

fn main() {
    let (mut x, mut y) = (0i64, 0i64);
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let Some((dir, n)) = parse_move(line.trim()) else {
            println!("invalid: {}", line.trim());
            continue;
        };
        let n = n as i64;
        match dir {
            'U' => y += n,
            'D' => y -= n,
            'L' => x -= n,
            _ => x += n,
        }
    }
    println!("position: ({x}, {y})");
}

Exercise 3 · write code

Undo stack with while let

Read words from stdin, one per line, pushing each onto a Vec<String> (a stack), except that the word undo pops the last pushed word instead (doing nothing if the stack is empty). At the end, print the remaining words from the top of the stack down, one per line, using while let Some(w) = stack.pop(). For the input a b undo c d undo undo e (one per line):

e
a

File: main.rs

Starting point

use std::io;

fn main() {
    let mut stack: Vec<String> = Vec::new();
    for line in io::stdin().lines() {
        let word = line.unwrap().trim().to_string();
    }
}

Example

input

a
b
undo
c
d
undo
undo
e

expected output

e
a

Example

input

undo
x

expected output

x
Hint 1

stack.pop() returns Option<String>: Some(last) or None when empty. Ignoring the result for undo is fine.

Hint 2

while let Some(w) = stack.pop() { println!("{w}"); } empties the stack from the top.

Reference solution
use std::io;

fn main() {
    let mut stack: Vec<String> = Vec::new();
    for line in io::stdin().lines() {
        let word = line.unwrap().trim().to_string();
        if word == "undo" {
            stack.pop();
        } else {
            stack.push(word);
        }
    }
    while let Some(w) = stack.pop() {
        println!("{w}");
    }
}

Exercise 4 · quiz

What does @ do?

What does this print?

fn main() {
    let age = 15;
    match age {
        n @ 13..=19 => println!("teen {n}"),
        n => println!("not a teen {n}"),
    }
}
Answer
  • teen 15

name @ pattern tests the value against the pattern AND binds it to the name, so you can use the matched value in the arm. Without @, a range pattern checks the value but gives you no variable; a plain name binds but checks nothing.