Control Flow · about 35 min

First match: Choosing Between Many Cases

Why this matters

When a value can be one of many cases (a menu choice, a command, a day of the week, a key code), a long if/else if chain becomes hard to read and easy to get wrong. match lists the cases as a table, and the compiler checks that the table is complete: forget a case and the program does not compile. match is one of Rust's most important features. This lesson introduces it on numbers and tuples; module 7 shows its full power with enums and destructuring.

The shape of a match

fn main() {
    let code = 404;
    let meaning = match code {
        200 => "OK",
        301 | 302 => "redirect",
        404 => "not found",
        500..=599 => "server error",
        _ => "something else",
    };
    println!("{code}: {meaning}");
}
404: not found

An arm's expression can be a block when it needs several statements:

fn main() {
    let choice = 2;
    match choice {
        1 => println!("starting"),
        2 => {
            println!("saving");
            println!("saved");
        }
        _ => println!("unknown choice"),
    }
}
saving
saved

(A comma after a block arm is optional.)

Exhaustiveness

A match must handle every possible value of the type. Leave a gap and rustc refuses:

fn main() {
    let n: u8 = 7;
    let size = match n {
        0 => "none",
        1..=9 => "a few",
        10..=99 => "many",
    };
    println!("{size}");
}
error[E0004]: non-exhaustive patterns: `100_u8..=u8::MAX` not covered
 --> main.rs:3:22
  |
3 |     let size = match n {
  |                      ^ pattern `100_u8..=u8::MAX` not covered
  |
  = note: the matched value is of type `u8`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
6 ~         10..=99 => "many",
7 ~         100_u8..=u8::MAX => todo!(),
  |

E0004 even tells you exactly which values are missing: 100 through 255. The compiler knows the full range of a u8 and checks your patterns against it. This becomes enormously valuable with enums (module 7): add a new variant to a type and the compiler points at every match in the program that does not handle it yet.

For numbers there are usually too many values to list, so a final _ arm handles "everything else". For a bool, true and false together are exhaustive.

Binding the value and adding guards

A plain name as a pattern matches anything and binds the value to that name, so the arm can use it. A guard, if condition after the pattern, adds an extra test:

fn classify(n: i32) -> String {
    match n {
        0 => String::from("zero"),
        x if x < 0 => format!("negative ({x})"),
        x if x % 2 == 0 => format!("even ({x})"),
        x => format!("odd ({x})"),
    }
}

fn main() {
    for n in [0, -4, 6, 7] {
        println!("{}", classify(n));
    }
}
zero
negative (-4)
even (6)
odd (7)

The last arm, x, catches everything left, so the match is exhaustive. Note that the compiler does not look inside guards when checking exhaustiveness; if all your arms have guards, you still need a catch-all. (format! builds a String, as in module 1; String::from makes one from a literal.)

Matching several values at once

Put values in a tuple and match on its shape:

fn main() {
    for (x, y) in [(0, 0), (3, 0), (0, -2), (1, 1)] {
        let place = match (x, y) {
            (0, 0) => "origin",
            (_, 0) => "on the x axis",
            (0, _) => "on the y axis",
            _ => "elsewhere",
        };
        println!("({x}, {y}) is {place}");
    }
}
(0, 0) is origin
(3, 0) is on the x axis
(0, -2) is on the y axis
(1, 1) is elsewhere

(_, 0) matches any tuple whose second element is 0. This style replaces nested ifs with a flat table that reads like a specification.

match on text

Strings can be matched against literals, which is perfect for simple commands:

fn main() {
    for cmd in ["start", "stop", "jump"] {
        match cmd {
            "start" | "go" => println!("starting"),
            "stop" => println!("stopping"),
            other => println!("unknown command: {other}"),
        }
    }
}
starting
stopping
unknown command: jump

For a String read from input, match on line.trim(), which gives the &str form the literals compare with (module 6 explains the difference between String and &str).

How match compiles

The compiler turns a match into whatever is fastest: a chain of comparisons for a few arms, a binary search over ranges, or a jump table (an array of code addresses indexed by the value) for dense integer cases. You write the clear version and the compiler picks the efficient one. A long else if chain usually ends up the same, but match states your intent and gets the exhaustiveness check.

Common mistakes

Unreachable arms. A wildcard or broad pattern before specific ones hides them:

fn main() {
    let n = 3;
    match n {
        _ => println!("anything"),
        3 => println!("three"),
    }
}
warning: unreachable pattern
 --> main.rs:5:9
  |
4 |         _ => println!("anything"),
  |         - matches any value
5 |         3 => println!("three"),
  |         ^ no value can reach this
  |
  = note: `#[warn(unreachable_patterns)]` on by default

It is a warning, not an error, but it is always a bug. Put specific patterns first.

Arms with different types: E0308 "match arms have incompatible types". Every arm of a match used as a value must produce the same type.

Using a variable as if it were a constant. match n { limit => ... } does not compare with a variable called limit; it creates a new binding named limit that matches everything. To compare against a variable, use a guard: x if x == limit =>. (Constants declared with const do work as patterns.)

Recap

Going further: rewrite the grade calculator from lesson 1 with a match on the score using range patterns, and see how the compiler reacts if you leave out a range. Module 7 continues the story with enums, destructuring structs and if let.

Exercises

Exercise 1 · write code

Day names

Read a number from stdin and print the matching day name, with 1 = Monday through 7 = Sunday. Any other number prints no such day. Use match.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: u32 = line.trim().parse().unwrap();
}

Example

input

1

expected output

Monday

Example

input

7

expected output

Sunday
Hint 1

Each arm is 1 => "Monday",; the last arm _ => "no such day", catches everything else.

Reference solution
use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: u32 = line.trim().parse().unwrap();
    let name = match n {
        1 => "Monday",
        2 => "Tuesday",
        3 => "Wednesday",
        4 => "Thursday",
        5 => "Friday",
        6 => "Saturday",
        7 => "Sunday",
        _ => "no such day",
    };
    println!("{name}");
}

Exercise 2 · write code

Cover every case

This function does not compile: error E0004, non-exhaustive patterns. Fix it by adding the missing arm so that every u8 is handled: values 0-9 are "low", 10-99 "medium", and 100 and above "high". (The return type &'static str means "a string literal", text that is built into the program and lives as long as it runs; module 10 explains the 'static part.) The given main prints:

low
medium
high
high

File: main.rs

Starting point

fn level(x: u8) -> &'static str {
    match x {
        0..=9 => "low",
        10..=99 => "medium",
    }
}

fn main() {
    println!("{}", level(3));
    println!("{}", level(42));
    println!("{}", level(100));
    println!("{}", level(255));
}

Example

expected output

low
medium
high
high
Hint 1

The error lists which values are not covered.

Hint 2

For a u8 you can cover the rest exactly with 100..=255, or with _.

Reference solution
fn level(x: u8) -> &'static str {
    match x {
        0..=9 => "low",
        10..=99 => "medium",
        100..=255 => "high",
    }
}

fn main() {
    println!("{}", level(3));
    println!("{}", level(42));
    println!("{}", level(100));
    println!("{}", level(255));
}

Exercise 3 · write code

FizzBuzz with a tuple match

Read n from stdin and print the numbers 1 to n, one per line, but print Fizz for multiples of 3, Buzz for multiples of 5 and FizzBuzz for multiples of both.

Solve it by matching on the tuple (i % 3, i % 5). For input 15 the last lines are:

13
14
FizzBuzz

File: main.rs

Starting point

use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: u32 = line.trim().parse().unwrap();
}

Example

input

15

expected output

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

Example

input

2

expected output

1
2
Hint 1

match (i % 3, i % 5) { (0, 0) => ..., (0, _) => ..., (_, 0) => ..., _ => ... }

Hint 2

Order matters: put (0, 0) first, since (0, _) also matches it.

Reference solution
use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: u32 = line.trim().parse().unwrap();
    for i in 1..=n {
        match (i % 3, i % 5) {
            (0, 0) => println!("FizzBuzz"),
            (0, _) => println!("Fizz"),
            (_, 0) => println!("Buzz"),
            _ => println!("{i}"),
        }
    }
}

Exercise 4 · quiz

Which arm runs?

What does this print?

fn main() {
    let n = 15;
    let s = match n {
        x if x % 5 == 0 => "five",
        x if x % 3 == 0 => "three",
        10..=20 => "teen",
        _ => "other",
    };
    println!("{s}");
}
Answer
  • five

Arms are tried top to bottom and the first that matches wins; the rest are not considered. 15 matches the first arm (the pattern x binds 15 and the guard x % 5 == 0 is true). Unlike C's switch, there is no fall-through into the next arm.