Patterns and Matching
An introduction to pattern matching in Rust, covering where patterns can be used, the concept of refutability, comprehensive pattern syntax, and a practical example of populating a binary tree using patterns.
Rust's pattern matching system is one of the features that sets the language apart. It goes far beyond a simple switch statement, allowing you to destructure complex data, bind variables, and enforce that every possible case is handled—all checked at compile time. The following sections explore every aspect of patterns and matching, from the places patterns can appear to the complete syntax you can use to express complex logic, finishing with a hands-on example using binary trees.
Where Patterns Can Be Used
Patterns don't just live inside match expressions. Rust sprinkles them throughout the language in places that, at first glance, don't even look like pattern context—let statements, function parameters, and for loops all use patterns to bind names to values. The dedicated page All Places Patterns Can Be Used walks through each location, explaining the rules and constraints that apply.
A quick preview shows just how pervasive patterns are:
let (x, y) = (10, 20); // tuple destructuring in let
if let Some(val) = optional_value { // refutable pattern in if let
println!("Got {}", val);
}
while let Ok(line) = reader.read_line() { // pattern in while let
// process each line
}
for (index, item) in vec.iter().enumerate() { // tuple pattern in for loop
println!("{}: {:?}", index, item);
}
Each construct expects a pattern that is appropriate for its context, and the page explains why.
Refutability: Why Some Patterns Can Fail
Not all patterns are created equal. Some match every possible value—for example, a bare variable x always succeeds. Others, like Some(5), will only match a subset of values. This distinction is called refutability. The compiler uses it to decide whether a pattern is legal in a given position: a let statement demands an irrefutable pattern because a variable must always be bound; if let and while let are designed for refutable patterns because they execute conditionally.
Compile Error:
Using a refutable pattern where an irrefutable one is expected—for instance, let Some(x) = maybe_value;—produces a compiler error. Rust refuses to compile code that could leave a variable unbound.
The full explanation, including how to decide which kind of pattern to use and what the compiler checks, is covered in Refutability: Whether a Pattern Might Fail to Match.
Pattern Syntax Overview
The expressive power of match and related constructs comes from the rich pattern syntax that Rust supports. It starts with the obvious—matching against literal numbers or strings—but quickly expands to include ranges, destructuring of structs, enums, and tuples, match guards (additional if conditions on an arm), and the ability to bind matched sub-values to variables. You can even combine patterns with |, ignore parts of a value with .. or _, and reference or match through references with ref/ref mut.
Here is a small sampler of what pattern syntax can express:
match point {
Point { x: 0, y } => println!("On the y axis at {}", y), // destructuring with literal
Point { x, y: 0 } => println!("On the x axis at {}", x),
Point { x, y } if x == y => println!("On the diagonal"), // match guard
Point { x, y } => println!("Somewhere else ({}, {})", x, y),
}
All Cases Covered:
If the compiler accepts your match expression without complaining about non-exhaustive patterns, you have successfully covered every possible variant of the type being matched. That guarantee is a core Rust safety pillar.
The full syntax, including all the forms listed above and more, is detailed in Pattern Syntax, where every variant is broken down with concrete use cases.
Practical Example: Building a Binary Tree
Seeing patterns in action with a recursive data structure makes their utility concrete. Consider a binary tree type defined as an enum:
#[derive(Debug)]
enum Tree<T> {
Empty,
Node(T, Box<Tree<T>>, Box<Tree<T>>),
}
A function to insert a value into a binary search tree can rely entirely on pattern matching to handle the two cases: an empty tree becomes a new node, and a non-empty tree compares the value to the current node and recursively inserts into the left or right child. The code is direct and readable because the pattern simultaneously checks the variant and extracts the node's value and children.
fn insert<T: Ord>(tree: Tree<T>, val: T) -> Tree<T> {
match tree {
Tree::Empty => Tree::Node(val, Box::new(Tree::Empty), Box::new(Tree::Empty)),
Tree::Node(v, left, right) => {
if val <= v {
Tree::Node(v, Box::new(insert(*left, val)), right)
} else {
Tree::Node(v, left, Box::new(insert(*right, val)))
}
}
}
}
The match destructively moves the tree, and on each arm we construct the new tree shape. Notice how the pattern Tree::Node(v, left, right) binds the three fields; the right-hand side reconstructs a Node with the appropriately updated subtree.
Exhaustiveness Matters:
Omitting the Tree::Empty arm here would yield a compiler error because the match would be non-exhaustive. The compiler’s insistence on handling Empty is what prevents null-pointer-like bugs in recursive code.
Running this code with a few insertions, then printing the tree, produces a nested Node structure showing the inserted values in their correct positions according to the ordering.
The dedicated page Populating a Binary Tree Using Patterns expands this example into a complete working program and discusses additional pattern techniques like using @ bindings and refutable matching on tree shapes.
Understanding patterns and matching is foundational to writing idiomatic Rust. The combination of destructuring, exhaustiveness guarantees, and the ability to place patterns almost anywhere makes it a tool you will reach for constantly. With the concepts introduced here, you will be equipped to use every form of pattern the language offers and apply them to real data structures.