Dec Code Story

Chapter #3 – Pattern Matching & Destructuring

exhaustive matching, nested patterns, guards, record destructuring

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