Pattern Matching
Destructuring and dispatching on data shape rather than explicit branching.
Overview
Pattern matching tests a value against a series of patterns and executes the corresponding branch when a match is found. Unlike switch/case, it can destructure nested data structures, bind inner values to variables, and guarantee exhaustiveness, forcing the developer to handle every possible case.
Origin
Pattern matching originated in ML (1973) and was central to Haskell, Erlang, and Scala. Ruby added it in version 3.0 (2020). JavaScript has a proposal (Stage 1 as of 2024). It became prominent in systems programming through Rust's match expression.
Examples
Ruby 3 pattern matching on API responses
response = { status: 200, body: { user: { id: 1, name: 'Jarred' } } }
case response
in { status: 200, body: { user: { id: Integer => id, name: String => name } } }
puts "User #{id}: #{name}"
in { status: 404 }
puts "Not found"
in { status: 401 | 403 }
puts "Unauthorised"
in { status: (500..), body: { error: String => msg } }
puts "Server error: #{msg}"
endThe => operator binds the matched value to a local variable. Integer and String act as type guards. The range (500..) matches any status >= 500.
Rust exhaustive 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()
}
}
}
// The compiler errors if any variant is unhandled.Rust's match is exhaustive at compile time. Adding a new Shape variant immediately breaks this function, forcing the developer to handle it, a safety guarantee that if/else chains cannot provide.
Use Cases
- 01Parsing and transforming nested JSON / API responses into typed structures
- 02State machines: matching on (current_state, event) pairs
- 03Error handling: branching on error type and extracting payload in one step
- 04Compiler and interpreter AST evaluation
- 05Protocol message handling where message type determines processing
When Not to Use
- //For simple boolean conditions, an if/else is more readable when there are only two paths
- //In languages where it is not idiomatic and the team lacks familiarity
- //When the pattern is only checking a single field, a regular conditional is faster to read
Technical Notes
- Exhaustiveness checking (compiler error on unhandled cases) is the critical advantage over if/else chains, it prevents silent regressions when new variants are added
- Ruby's pattern matching is not exhaustive by default; use case/in without an else to raise NoMatchingPatternError on unmatched input
- Performance: pattern matching compiles to efficient decision trees in Rust and Haskell. In Ruby it is interpreted at runtime and slightly slower than explicit conditionals
- Find patterns (in [*, target, *]) allow matching elements anywhere in an array without knowing position
More in Programming Techniques