ExamplesBy LevelBy TopicLearning Paths
926 Intermediate

926-pattern-matching — Pattern Matching

Functional Programming

Tutorial

The Problem

Pattern matching is the primary control flow mechanism of functional programming. Where imperative languages use if/else chains and switch statements, functional languages match on the structure of data: a tree is either a Leaf or a Node(left, value, right); a result is either Ok(x) or Err(e). OCaml and Rust both center pattern matching as a first-class language feature. Both compile match expressions to decision trees, check exhaustiveness at compile time (no unhandled case), and bind variables in each arm. This example uses algebraic shapes to compare the two languages' match syntax and capabilities.

🎯 Learning Outcomes

  • • Write exhaustive match expressions on Rust enums
  • • Use guard clauses (if condition) inside match arms for refined patterns
  • • Use nested pattern destructuring to bind inner values
  • • Recognize that Rust's match is exhaustive — the compiler rejects missing cases
  • • Compare Rust's match with OCaml's match (nearly identical syntax)
  • Code Example

    enum Shape { Circle(f64), Rectangle(f64, f64) }
    
    fn area(shape: &Shape) -> f64 {
        match shape {
            Shape::Circle(r) => PI * r * r,
            Shape::Rectangle(w, h) => w * h,
        }
    }

    Key Differences

  • Syntax similarity: OCaml when = Rust if in guards; OCaml | variant = Rust , for multiple patterns. Overall extremely similar.
  • Reference patterns: Rust requires &Shape::Circle(r) when matching &Shape; OCaml automatically dereferences values.
  • Exhaustiveness: Both enforce exhaustive matching; Rust uses _ wildcard, OCaml uses _ or named wildcards.
  • Struct patterns: Rust struct fields can be matched with { field_name: pattern }; OCaml records use { field_name = pattern }.
  • OCaml Approach

    OCaml's match shape with | Circle r -> pi *. r *. r | Rectangle(w, h) -> w *. h | Triangle(a, b, c) -> ... is nearly identical. Guard clauses: | Rectangle(w, h) when w = h -> .... OCaml's when keyword corresponds to Rust's if in match guards. Both compile to efficient decision trees. OCaml also has function keyword as shorthand for fun x -> match x with. The syntax is so similar that OCaml knowledge transfers almost directly to Rust pattern matching.

    Full Source

    #![allow(clippy::all)]
    /// Pattern Matching: the heart of both OCaml and Rust.
    ///
    /// Both languages use pattern matching as a primary control flow mechanism.
    /// OCaml has algebraic data types; Rust has enums. The mapping is remarkably direct.
    
    // ── Define a Shape type (algebraic data type / enum) ────────────────────────
    
    #[derive(Debug, Clone, PartialEq)]
    pub enum Shape {
        Circle(f64),             // radius
        Rectangle(f64, f64),     // width, height
        Triangle(f64, f64, f64), // three sides
    }
    
    // ── Idiomatic Rust: match expressions ───────────────────────────────────────
    
    /// Calculate area using match — direct analog of OCaml's pattern matching
    pub 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) => {
                // Heron's formula
                let s = (a + b + c) / 2.0;
                (s * (s - a) * (s - b) * (s - c)).sqrt()
            }
        }
    }
    
    /// Describe a shape — demonstrates string formatting in match arms
    pub fn describe(shape: &Shape) -> String {
        match shape {
            Shape::Circle(r) => format!("Circle with radius {r}"),
            Shape::Rectangle(w, h) if (w - h).abs() < f64::EPSILON => {
                format!("Square with side {w}")
            }
            Shape::Rectangle(w, h) => format!("Rectangle {w}×{h}"),
            Shape::Triangle(a, b, c)
                if (a - b).abs() < f64::EPSILON && (b - c).abs() < f64::EPSILON =>
            {
                format!("Equilateral triangle with side {a}")
            }
            Shape::Triangle(a, b, c) => format!("Triangle with sides {a}, {b}, {c}"),
        }
    }
    
    // ── Nested pattern matching with Option ─────────────────────────────────────
    
    /// Find the largest shape by area from an optional list
    pub fn largest_area(shapes: &[Shape]) -> Option<f64> {
        // Uses iterator + fold, but the interesting bit is Option handling
        shapes.iter().map(area).fold(None, |max, a| match max {
            None => Some(a),
            Some(m) if a > m => Some(a),
            _ => max,
        })
    }
    
    // ── Recursive style with exhaustive matching ────────────────────────────────
    
    /// Count shapes of each type — recursive traversal with pattern matching
    pub fn count_by_type(shapes: &[Shape]) -> (usize, usize, usize) {
        fn aux(shapes: &[Shape], c: usize, r: usize, t: usize) -> (usize, usize, usize) {
            match shapes.split_first() {
                None => (c, r, t),
                Some((Shape::Circle(_), rest)) => aux(rest, c + 1, r, t),
                Some((Shape::Rectangle(_, _), rest)) => aux(rest, c, r + 1, t),
                Some((Shape::Triangle(_, _, _), rest)) => aux(rest, c, r, t + 1),
            }
        }
        aux(shapes, 0, 0, 0)
    }
    
    // ── Functional style with iterators ─────────────────────────────────────────
    
    /// Scale all shapes by a factor — map with pattern matching inside
    pub fn scale_all(shapes: &[Shape], factor: f64) -> Vec<Shape> {
        shapes
            .iter()
            .map(|s| match s {
                Shape::Circle(r) => Shape::Circle(r * factor),
                Shape::Rectangle(w, h) => Shape::Rectangle(w * factor, h * factor),
                Shape::Triangle(a, b, c) => Shape::Triangle(a * factor, b * factor, c * factor),
            })
            .collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::f64::consts::PI;
    
        #[test]
        fn test_area_circle() {
            let c = Shape::Circle(5.0);
            assert!((area(&c) - PI * 25.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_area_rectangle() {
            assert!((area(&Shape::Rectangle(3.0, 4.0)) - 12.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_area_triangle() {
            // 3-4-5 right triangle, area = 6
            assert!((area(&Shape::Triangle(3.0, 4.0, 5.0)) - 6.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_describe_with_guards() {
            assert_eq!(describe(&Shape::Rectangle(5.0, 5.0)), "Square with side 5");
            assert_eq!(
                describe(&Shape::Triangle(3.0, 3.0, 3.0)),
                "Equilateral triangle with side 3"
            );
            assert!(describe(&Shape::Rectangle(3.0, 4.0)).contains("×"));
        }
    
        #[test]
        fn test_largest_area_empty() {
            assert_eq!(largest_area(&[]), None);
        }
    
        #[test]
        fn test_largest_area_nonempty() {
            let shapes = vec![Shape::Circle(1.0), Shape::Rectangle(10.0, 10.0)];
            assert!((largest_area(&shapes).unwrap() - 100.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_count_by_type() {
            let shapes = vec![
                Shape::Circle(1.0),
                Shape::Circle(2.0),
                Shape::Rectangle(1.0, 2.0),
                Shape::Triangle(3.0, 4.0, 5.0),
            ];
            assert_eq!(count_by_type(&shapes), (2, 1, 1));
            assert_eq!(count_by_type(&[]), (0, 0, 0));
        }
    
        #[test]
        fn test_scale() {
            let shapes = vec![Shape::Circle(2.0), Shape::Rectangle(3.0, 4.0)];
            let scaled = scale_all(&shapes, 2.0);
            assert_eq!(scaled[0], Shape::Circle(4.0));
            assert_eq!(scaled[1], Shape::Rectangle(6.0, 8.0));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::f64::consts::PI;
    
        #[test]
        fn test_area_circle() {
            let c = Shape::Circle(5.0);
            assert!((area(&c) - PI * 25.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_area_rectangle() {
            assert!((area(&Shape::Rectangle(3.0, 4.0)) - 12.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_area_triangle() {
            // 3-4-5 right triangle, area = 6
            assert!((area(&Shape::Triangle(3.0, 4.0, 5.0)) - 6.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_describe_with_guards() {
            assert_eq!(describe(&Shape::Rectangle(5.0, 5.0)), "Square with side 5");
            assert_eq!(
                describe(&Shape::Triangle(3.0, 3.0, 3.0)),
                "Equilateral triangle with side 3"
            );
            assert!(describe(&Shape::Rectangle(3.0, 4.0)).contains("×"));
        }
    
        #[test]
        fn test_largest_area_empty() {
            assert_eq!(largest_area(&[]), None);
        }
    
        #[test]
        fn test_largest_area_nonempty() {
            let shapes = vec![Shape::Circle(1.0), Shape::Rectangle(10.0, 10.0)];
            assert!((largest_area(&shapes).unwrap() - 100.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_count_by_type() {
            let shapes = vec![
                Shape::Circle(1.0),
                Shape::Circle(2.0),
                Shape::Rectangle(1.0, 2.0),
                Shape::Triangle(3.0, 4.0, 5.0),
            ];
            assert_eq!(count_by_type(&shapes), (2, 1, 1));
            assert_eq!(count_by_type(&[]), (0, 0, 0));
        }
    
        #[test]
        fn test_scale() {
            let shapes = vec![Shape::Circle(2.0), Shape::Rectangle(3.0, 4.0)];
            let scaled = scale_all(&shapes, 2.0);
            assert_eq!(scaled[0], Shape::Circle(4.0));
            assert_eq!(scaled[1], Shape::Rectangle(6.0, 8.0));
        }
    }

    Deep Comparison

    Pattern Matching: OCaml vs Rust

    The Core Insight

    Pattern matching is where OCaml and Rust feel most alike. Both languages use algebraic data types (OCaml variants / Rust enums) with exhaustive matching — the compiler ensures every case is handled. This eliminates entire classes of bugs that plague languages without sum types.

    OCaml Approach

    OCaml's variant types and match/function expressions are the language's crown jewel:

    type shape = Circle of float | Rectangle of float * float
    
    let area = function
      | Circle r -> Float.pi *. r *. r
      | Rectangle (w, h) -> w *. h
    

    Guard clauses (when) add conditional logic within patterns. The compiler warns on non-exhaustive matches and unused cases — a safety net that catches bugs at compile time.

    Rust Approach

    Rust's enum + match is a direct descendant of ML-family pattern matching:

    enum Shape { Circle(f64), Rectangle(f64, f64) }
    
    fn area(shape: &Shape) -> f64 {
        match shape {
            Shape::Circle(r) => PI * r * r,
            Shape::Rectangle(w, h) => w * h,
        }
    }
    

    Rust adds ownership to the mix: matching can move, borrow, or copy inner values. The ref keyword and & patterns control this explicitly.

    Key Differences

    AspectOCamlRust
    Sum typestype t = A \| B of intenum T { A, B(i32) }
    ExhaustivenessCompiler warningCompiler error (stricter)
    Guardswhen clauseif guard
    BindingAutomatic copyMove/borrow semantics
    Nested matchNaturalNatural
    Or-patternsA \| B -> ...A \| B => ...
    Wildcard__
    function sugarYes (one-arg match)No equivalent

    What Rust Learners Should Notice

  • Exhaustiveness is enforced: Unlike OCaml's warning, Rust makes non-exhaustive matches a hard error. This is stricter and safer.
  • Ownership in patterns: When you match on Shape::Circle(r), r is a copy (for f64) or a move (for String). Use &Shape::Circle(r) or ref to borrow.
  • • **No function keyword**: OCaml's let f = function | A -> ... | B -> ... has no Rust equivalent. You always write fn f(x: T) -> U { match x { ... } }.
  • Guards work the same: Some(x) if x > 0 => ... in Rust mirrors OCaml's Some x when x > 0 -> ....
  • Further Reading

  • • [The Rust Book — Patterns and Matching](https://doc.rust-lang.org/book/ch18-00-patterns-and-matching.html)
  • • [OCaml Manual — Pattern Matching](https://v2.ocaml.org/manual/patterns.html)
  • • [Rust Reference — Enum types](https://doc.rust-lang.org/reference/items/enumerations.html)
  • Exercises

  • Add a Ellipse(f64, f64) variant for semi-major and semi-minor axes and add it to both the area and describe functions.
  • Implement perimeter using pattern matching, including Heron's formula for the triangle perimeter check (triangle inequality).
  • Write classify_shape(shape: &Shape) -> &str that returns "compact", "elongated", or "regular" based on the aspect ratio of each shape type.
  • Open Source Repos