BasicsDecCodeStory_Patterns.html
copyright © James Fawcett
Revised: 05/11/2026
3.0 Prologue
Pattern matching is the primary way to consume algebraic data types. It combines
case analysis (which variant?) with destructuring (what are the fields?) into a
single syntactic form. When the compiler enforces exhaustiveness, every possible
value must be handled, turning missing cases into compile errors rather than runtime
surprises.
3.1 Exhaustive Structural Matching
An exhaustive match covers every variant of a sum type. Adding a
new variant to the type causes all non-exhaustive matches to fail to compile,
prompting the programmer to handle the new case everywhere it matters.
-- Haskell: case expression must cover all constructors
area :: Shape -> Double
area shape = case shape of
Circle c r -> pi * r * r
Rect _ w h -> w * h
Triangle a b c -> triangleArea a b c
-- omitting Triangle would be a compile warning (GHC -Wall: error)
-- Rust: match must be exhaustive (always enforced, not just with -Wall)
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle { radius, .. } => std::f64::consts::PI * radius * radius,
Shape::Rect { width, height, .. } => width * height,
Shape::Triangle(a, b, c) => triangle_area(a, b, c),
}
}
The wildcard _ or a catch-all variable arm can cover remaining variants,
but doing so opts out of exhaustiveness for those cases - useful for large enums
where only a few variants are interesting.
3.2 Nested Patterns
Patterns compose: a pattern can match a constructor whose fields are themselves
matched by sub-patterns, all in a single expression without intermediate bindings.
-- Haskell: nested pattern matches on list structure
describeList :: [a] -> String
describeList xs = case xs of
[] -> "empty"
[_] -> "singleton"
[_, _] -> "pair"
(_:_:_:_) -> "three or more"
-- Matching nested ADT: a Result containing an Option
-- Rust
match result {
Ok(Some(value)) => process(value),
Ok(None) => handle_empty(),
Err(e) => handle_error(e),
}
-- F#: nested discriminated union pattern
match response with
| Ok (Some value) -> process value
| Ok None -> handleEmpty ()
| Error e -> handleError e
Nested patterns eliminate chains of nested if/else or multiple levels of
unwrap/match. The entire case analysis is expressed as a
single, readable decision tree.
3.3 Pattern Guards
A guard adds a boolean condition to a pattern arm. The arm matches
only when both the structural pattern and the guard condition are satisfied.
-- Haskell: guard syntax with |
classify :: Int -> String
classify n
| n < 0 = "negative"
| n == 0 = "zero"
| n < 10 = "small positive"
| otherwise = "large positive"
-- Haskell: guard on a case arm
describeTemp :: Double -> String
describeTemp t = case t of
t | t < 0 -> "freezing"
| t < 20 -> "cold"
| t < 30 -> "comfortable"
| otherwise -> "hot"
-- Rust: guard with if
match score {
s if s >= 90 => "A",
s if s >= 80 => "B",
s if s >= 70 => "C",
_ => "F",
}
Guards do not affect exhaustiveness checking: the compiler still requires that the
patterns themselves (ignoring guards) cover all cases. If all guards on a variant
could fail, the compiler may warn about a non-exhaustive match.
3.4 Record Destructuring
Record destructuring binds field names directly from a record or
struct without manually extracting each field. It is a pattern that matches a product
type by name rather than by position.
-- Haskell: record pattern
greet :: Person -> String
greet Person { name = n, age = a } = n ++ " is " ++ show a
-- F#: record destructuring in a let binding
let { Name = name; Age = age } = person
printfn "%s is %d" name age
-- Rust: struct destructuring in match and let
let Point { x, y } = point; // binds x and y
match event {
MouseClick { x, y, button } if button == Left => handle_click(x, y),
KeyPress { key, modifiers } => handle_key(key, modifiers),
_ => (),
}
-- Python: (limited) tuple unpacking
x, y = point # positional only
name, *rest = items # head/tail split
Destructuring in function parameters is particularly ergonomic in Haskell and F#,
where a function’s signature can simultaneously name and destructure its argument
in the same declaration.
3.5 Epilogue
Pattern matching transforms the consumption of structured data from a sequence of
field-access and type-check operations into a single declarative case analysis. It
is the syntactic counterpart to algebraic data types: ADTs describe the structure;
patterns exploit it. The next chapter examines how functions themselves become
first-class values in declarative languages.
3.6 References
Haskell Tutorial - Pattern Matching
F# Pattern Matching - Microsoft
Rust Patterns - The Book
PEP 634 - Python Structural Pattern Matching