926-pattern-matching — Pattern Matching
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
match expressions on Rust enumsif condition) inside match arms for refined patternsmatch 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
when = Rust if in guards; OCaml | variant = Rust , for multiple patterns. Overall extremely similar.&Shape::Circle(r) when matching &Shape; OCaml automatically dereferences values._ wildcard, OCaml uses _ or named wildcards.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));
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| Sum types | type t = A \| B of int | enum T { A, B(i32) } |
| Exhaustiveness | Compiler warning | Compiler error (stricter) |
| Guards | when clause | if guard |
| Binding | Automatic copy | Move/borrow semantics |
| Nested match | Natural | Natural |
| Or-patterns | A \| B -> ... | A \| B => ... |
| Wildcard | _ | _ |
function sugar | Yes (one-arg match) | No equivalent |
What Rust Learners Should Notice
Shape::Circle(r), r is a copy (for f64) or a move (for String). Use &Shape::Circle(r) or ref to borrow.function keyword**: OCaml's let f = function | A -> ... | B -> ... has no Rust equivalent. You always write fn f(x: T) -> U { match x { ... } }.Some(x) if x > 0 => ... in Rust mirrors OCaml's Some x when x > 0 -> ....Further Reading
Exercises
Ellipse(f64, f64) variant for semi-major and semi-minor axes and add it to both the area and describe functions.perimeter using pattern matching, including Heron's formula for the triangle perimeter check (triangle inequality).classify_shape(shape: &Shape) -> &str that returns "compact", "elongated", or "regular" based on the aspect ratio of each shape type.