Pattern Matching
A match block in Rust is the triage nurse at a hospital intake desk. A patient walks in, the nurse reads off a printed list of conditions from top to bottom, and the first row that fits the patient sends them to a room. The list cannot have holes. If a patient shows up who matches no row, the nurse cannot just shrug — the compiler refuses to print the list at all until every possible patient has somewhere to go. That guarantee is the whole point. You can read the entire decision flow on one page and know nothing slips past it.

The idea is older than Rust by sixty years. In 1960 a British logician named Rod Burstall was working on a language called NPL at the University of Edinburgh. His group wanted a clean way to take apart a piece of data — a list, a tree, a record — without writing a chain of if checks that the reader had to mentally untangle. They invented pattern matching, where you draw a little picture of the shape you expect and the compiler peels the data apart along that shape. The idea jumped to ML in 1973, to Miranda in 1985, to Haskell in 1990, and Graydon Hoare pulled it straight into Rust when he started the project at Mozilla. He kept the exhaustiveness check from ML on purpose, because every bug he had seen in a switch statement in C came from a missing branch.
Here is the patient list — a Shape enum with four kinds. Each kind carries the data the nurse needs to decide what to do.
enum Shape {
Circle { radius: f64 },
Square { side: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}The enum says a shape is exactly one of four things and nothing else. No fifth shape can sneak in. Now the triage list. The nurse walks it from top to bottom and the first arm that fits returns the diagnosis.
fn classify(s: &Shape) -> &'static str {
match s {
Shape::Circle { radius } if *radius < 1.0 => "tiny circle",
Shape::Circle { .. } => "circle",
Shape::Square { side } if *side == 0.0 => "empty square",
Shape::Square { .. } => "square",
Shape::Rectangle { width, height } if width == height => "square-ish rectangle",
Shape::Rectangle { .. } => "rectangle",
Shape::Triangle { .. } => "triangle",
}
}Two things in that block are doing the heavy lifting. The first is the pattern itself — Shape::Circle { radius } does not just check that the shape is a circle, it pulls the radius out and names it on the spot so the right side of the arrow can use it. The second is the word if after the pattern. That is a guard. A guard is an extra condition the nurse layers on top of the shape — "yes it is a circle, but only count it as a tiny one if the radius is under 1.0." Without guards, you would write a separate enum case for every tiny variation. With guards, the shape stays clean and the rules live next to the decision.
Notice the order. Shape::Circle { radius } if *radius < 1.0 sits above Shape::Circle { .. } on purpose. If you flipped them, the plain circle arm would catch every circle first and the tiny-circle row would never fire. The nurse reads top to bottom. You order from specific to general, every time.
Now the part the compiler enforces. Delete the Shape::Triangle { .. } arm and try to build. The compiler stops you with a message that says "patterns Shape::Triangle { .. } not covered." It will not let you ship a match that has a hole. This is the exhaustiveness check Hoare borrowed from ML. In C, a missing case in a switch silently falls through and your program does something nobody planned for. In Rust, the program does not compile until you handle every variant. The .. inside the braces means "I do not care about the fields," and the bare _ arm means "I do not care which variant" — both let you cover the rest with one line when you genuinely have nothing different to do. Use them on purpose, not to silence the compiler.
The main function builds seven patients, hands each to the classifier, and prints what came back.
fn main() {
let shapes = [
Shape::Circle { radius: 0.5 },
Shape::Circle { radius: 4.0 },
Shape::Square { side: 0.0 },
Shape::Square { side: 3.0 },
Shape::Rectangle { width: 2.0, height: 2.0 },
Shape::Rectangle { width: 4.0, height: 7.0 },
Shape::Triangle { base: 3.0, height: 4.0 },
];
println!("decision tree:");
for s in &shapes {
println!(" {} -> {}", short(s), classify(s));
}
}
fn short(s: &Shape) -> String {
match s {
Shape::Circle { radius } => format!("Circle(r={radius})"),
Shape::Square { side } => format!("Square(s={side})"),
Shape::Rectangle { width, height } => format!("Rectangle({width}x{height})"),
Shape::Triangle { base, height } => format!("Triangle(b={base},h={height})"),
}
}Run it and the decision tree falls out.
decision tree:
Circle(r=0.5) -> tiny circle
Circle(r=4) -> circle
Square(s=0) -> empty square
Square(s=3) -> square
Rectangle(2x2) -> square-ish rectangle
Rectangle(4x7) -> rectangle
Triangle(b=3,h=4) -> triangleRead down the output and you can see every guard fire. The radius-0.5 circle hits the if *radius < 1.0 arm and lands on "tiny circle." The radius-4 circle skips that guard and lands on the plain circle row below it. The 2x2 rectangle has equal width and height, so it triggers the if width == height guard before the plain rectangle row catches everything else. Each row of stdout is one walk down the triage list.
One question worth asking — what stops you from writing Shape::Square { side } if *side == 0.0 for "empty square" and then forgetting the rule that squares with side zero are not really squares? Nothing, in the patterns themselves. The compiler checks that you covered every variant. It does not check that your guards make sense. Guards are your business. The exhaustiveness check buys you the floor; you are still responsible for the rules you write on top of it.

The thing match cannot do is reach across data that is owned somewhere else and decide based on it. The patient list lives in one place, and to look up something stored in a function on the other side of the program, you need a way to pass values around with rules about who owns them — which is the next bottleneck.