ExamplesBy LevelBy TopicLearning Paths
003 Intermediate

003 — Pattern Matching

Functional Programming

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

  • • Use match for tuples, enums, and nested data structures
  • • Write match guards (if n < 0) to add conditions beyond structural patterns
  • • Understand how Rust's exhaustiveness checking catches missing cases at compile time
  • • Build recursive algebraic data types with enum and Box
  • • Evaluate expression trees using pattern matching recursion
  • • Write match arms with multiple patterns using |: Shape::Circle(_) | Shape::Rectangle(_, _) => "has area" to group related cases
  • Code 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 for recursion: Rust requires 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.
  • Guard syntax: Both use guards (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.
  • Wildcard binding: Rust uses _ for unused bindings and .. to ignore multiple tuple fields. OCaml uses _ the same way.
  • OR patterns: OCaml allows | A | B -> expr in one arm. Rust also supports A | B => expr since Rust 1.53.
  • Exhaustiveness: Both languages enforce exhaustive matching at compile time. A missing variant is a compile error in Rust (non_exhaustive patterns) and a warning in OCaml. Exhaustiveness eliminates entire classes of runtime bugs.
  • Box for recursion: Rust requires 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.
  • Guards: Both use 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: Rust's @ 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");
        }
    }
    ✓ Tests Rust test suite
    #[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 syntax
  • • Variant types matched directly
  • • Tuple destructuring in patterns
  • when guards for conditional matching
  • _ wildcard, as binding
  • Rust Approach

  • match expr { pattern => body } syntax
  • enum variants matched with Enum::Variant
  • • Tuple and struct destructuring
  • if guards in match arms
  • _ wildcard, @ binding
  • matches! macro for boolean checks
  • Comparison Table

    FeatureOCamlRust
    Syntaxmatch x with \| p -> ematch x { p => e }
    Guardwhen conditionif condition
    Wildcard__
    Bindingas namename @
    Or-patternp1 \| p2p1 \| p2
    ExhaustiveYes (compiler)Yes (compiler)

    Exercises

  • Extend Shape: Add a 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 of Expr: Write depth(expr: &Expr) -> usize that returns the maximum nesting depth of an expression tree, using pattern matching recursion.
  • Simplify: Write 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.
  • Simplify expressions: Add 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).
  • Pretty print: Implement fmt::Display for Expr that produces a human-readable infix expression like (1 + (2 * 3)) with proper parenthesization.
  • Open Source Repos