003 — Pattern Matching
Tutorial
The Problem
Pattern matching originated in ML (1973) and is now recognized as one of the most productive features in statically typed programming. It combines structural decomposition of data with exhaustiveness checking: the compiler verifies that every possible shape of a value is handled, preventing entire classes of runtime errors that plague switch/case in other languages.
Without pattern matching, processing tree-structured data (ASTs, JSON, XML) or discriminated unions requires chains of instanceof checks and casts. Pattern matching makes this both safe and concise, which is why it has spread to Swift, Kotlin, Python 3.10, C# 7, and even Java 21.
🎯 Learning Outcomes
match for tuples, enums, and nested data structuresif n < 0) to add conditions beyond structural patternsenum and Box|: Shape::Circle(_) | Shape::Rectangle(_, _) => "has area" to group related casesCode Example
#![allow(clippy::all)]
// 003: Pattern Matching
// Tuples, enums, nested patterns, guards
// Approach 1: Simple tuple matching
fn describe_pair(pair: (i32, i32)) -> String {
match pair {
(0, 0) => "origin".to_string(),
(x, 0) => format!("x-axis at {}", x),
(0, y) => format!("y-axis at {}", y),
(x, y) => format!("point ({}, {})", x, y),
}
}
// Approach 2: Enum matching
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle(a, b, c) => {
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
fn shape_name(shape: &Shape) -> &str {
match shape {
Shape::Circle(_) => "circle",
Shape::Rectangle(_, _) => "rectangle",
Shape::Triangle(_, _, _) => "triangle",
}
}
// Approach 3: Nested patterns with guards
enum Expr {
Num(i32),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
}
fn eval(expr: &Expr) -> i32 {
match expr {
Expr::Num(n) => *n,
Expr::Add(a, b) => eval(a) + eval(b),
Expr::Mul(a, b) => eval(a) * eval(b),
}
}
fn classify_number(n: i32) -> &'static str {
match n {
n if n < 0 => "negative",
0 => "zero",
n if n % 2 == 0 => "positive even",
_ => "positive odd",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_pair() {
assert_eq!(describe_pair((0, 0)), "origin");
assert_eq!(describe_pair((3, 0)), "x-axis at 3");
assert_eq!(describe_pair((0, 5)), "y-axis at 5");
assert_eq!(describe_pair((2, 3)), "point (2, 3)");
}
#[test]
fn test_area() {
assert!((area(&Shape::Circle(1.0)) - std::f64::consts::PI).abs() < 0.001);
assert_eq!(area(&Shape::Rectangle(3.0, 4.0)), 12.0);
}
#[test]
fn test_shape_name() {
assert_eq!(shape_name(&Shape::Circle(1.0)), "circle");
}
#[test]
fn test_eval() {
let e = Expr::Add(
Box::new(Expr::Num(1)),
Box::new(Expr::Mul(Box::new(Expr::Num(2)), Box::new(Expr::Num(3)))),
);
assert_eq!(eval(&e), 7);
}
#[test]
fn test_classify() {
assert_eq!(classify_number(-5), "negative");
assert_eq!(classify_number(0), "zero");
assert_eq!(classify_number(4), "positive even");
assert_eq!(classify_number(7), "positive odd");
}
}Key Differences
Box<Expr> in recursive enum variants because the compiler needs to know the size. OCaml allocates on the heap automatically via the GC; no boxing syntax needed.if condition) but OCaml omits the variable binding in guards when already in scope: | n when n < 0 -> "negative". Rust requires n if n < 0._ for unused bindings and .. to ignore multiple tuple fields. OCaml uses _ the same way.| A | B -> expr in one arm. Rust also supports A | B => expr since Rust 1.53.non_exhaustive patterns) and a warning in OCaml. Exhaustiveness eliminates entire classes of runtime bugs.Box<T> in recursive enum variants to give them a known size. OCaml allocates all values on the heap transparently — recursive types need no annotation.if guards in pattern arms (n if n < 0 => ... in Rust, n when n < 0 -> in OCaml), but Rust's guards don't affect exhaustiveness checking.@ binding (n @ 1..=10) and OCaml's as binding (x :: _ as list) allow naming matched substructures while still pattern-matching.OCaml Approach
OCaml uses the same match syntax and the same structural patterns. Variant types (type shape = Circle of float | Rectangle of float * float) are OCaml's algebraic data types. The function keyword is shorthand for fun x -> match x with .... OCaml's pattern matching is exhaustiveness-checked at compile time just like Rust's — a missing arm is a warning or error, not a silent bug.
Full Source
#![allow(clippy::all)]
// 003: Pattern Matching
// Tuples, enums, nested patterns, guards
// Approach 1: Simple tuple matching
fn describe_pair(pair: (i32, i32)) -> String {
match pair {
(0, 0) => "origin".to_string(),
(x, 0) => format!("x-axis at {}", x),
(0, y) => format!("y-axis at {}", y),
(x, y) => format!("point ({}, {})", x, y),
}
}
// Approach 2: Enum matching
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle(a, b, c) => {
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
fn shape_name(shape: &Shape) -> &str {
match shape {
Shape::Circle(_) => "circle",
Shape::Rectangle(_, _) => "rectangle",
Shape::Triangle(_, _, _) => "triangle",
}
}
// Approach 3: Nested patterns with guards
enum Expr {
Num(i32),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
}
fn eval(expr: &Expr) -> i32 {
match expr {
Expr::Num(n) => *n,
Expr::Add(a, b) => eval(a) + eval(b),
Expr::Mul(a, b) => eval(a) * eval(b),
}
}
fn classify_number(n: i32) -> &'static str {
match n {
n if n < 0 => "negative",
0 => "zero",
n if n % 2 == 0 => "positive even",
_ => "positive odd",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_pair() {
assert_eq!(describe_pair((0, 0)), "origin");
assert_eq!(describe_pair((3, 0)), "x-axis at 3");
assert_eq!(describe_pair((0, 5)), "y-axis at 5");
assert_eq!(describe_pair((2, 3)), "point (2, 3)");
}
#[test]
fn test_area() {
assert!((area(&Shape::Circle(1.0)) - std::f64::consts::PI).abs() < 0.001);
assert_eq!(area(&Shape::Rectangle(3.0, 4.0)), 12.0);
}
#[test]
fn test_shape_name() {
assert_eq!(shape_name(&Shape::Circle(1.0)), "circle");
}
#[test]
fn test_eval() {
let e = Expr::Add(
Box::new(Expr::Num(1)),
Box::new(Expr::Mul(Box::new(Expr::Num(2)), Box::new(Expr::Num(3)))),
);
assert_eq!(eval(&e), 7);
}
#[test]
fn test_classify() {
assert_eq!(classify_number(-5), "negative");
assert_eq!(classify_number(0), "zero");
assert_eq!(classify_number(4), "positive even");
assert_eq!(classify_number(7), "positive odd");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_pair() {
assert_eq!(describe_pair((0, 0)), "origin");
assert_eq!(describe_pair((3, 0)), "x-axis at 3");
assert_eq!(describe_pair((0, 5)), "y-axis at 5");
assert_eq!(describe_pair((2, 3)), "point (2, 3)");
}
#[test]
fn test_area() {
assert!((area(&Shape::Circle(1.0)) - std::f64::consts::PI).abs() < 0.001);
assert_eq!(area(&Shape::Rectangle(3.0, 4.0)), 12.0);
}
#[test]
fn test_shape_name() {
assert_eq!(shape_name(&Shape::Circle(1.0)), "circle");
}
#[test]
fn test_eval() {
let e = Expr::Add(
Box::new(Expr::Num(1)),
Box::new(Expr::Mul(Box::new(Expr::Num(2)), Box::new(Expr::Num(3)))),
);
assert_eq!(eval(&e), 7);
}
#[test]
fn test_classify() {
assert_eq!(classify_number(-5), "negative");
assert_eq!(classify_number(0), "zero");
assert_eq!(classify_number(4), "positive even");
assert_eq!(classify_number(7), "positive odd");
}
}
Deep Comparison
Core Insight
Pattern matching is the backbone of both languages. The compiler enforces exhaustiveness — you must handle every case. Both support destructuring, guards, wildcards, and nested patterns.
OCaml Approach
match expr with | pattern -> body syntaxwhen guards for conditional matching_ wildcard, as bindingRust Approach
match expr { pattern => body } syntaxenum variants matched with Enum::Variantif guards in match arms_ wildcard, @ bindingmatches! macro for boolean checksComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Syntax | match x with \| p -> e | match x { p => e } |
| Guard | when condition | if condition |
| Wildcard | _ | _ |
| Binding | as name | name @ |
| Or-pattern | p1 \| p2 | p1 \| p2 |
| Exhaustive | Yes (compiler) | Yes (compiler) |
Exercises
Triangle variant with base and height to the Shape enum and add a perimeter function. Verify the compiler forces you to handle the new case everywhere.depth(expr: &Expr) -> usize that returns the maximum nesting depth of an expression tree, using pattern matching recursion.simplify(expr: Expr) -> Expr that eliminates Add(Num(0), x) and Mul(Num(1), x) identity cases — the kind of optimization a real compiler's peephole pass performs.Expr::simplify(&self) -> Expr that applies basic algebraic rules: Add(x, Num(0)) = x, Mul(x, Num(1)) = x, Mul(x, Num(0)) = Num(0).fmt::Display for Expr that produces a human-readable infix expression like (1 + (2 * 3)) with proper parenthesization.