100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Pattern Matching in F#

Understand how F#'s match expression destructures values, enforces exhaustiveness, and replaces verbose conditional logic with concise, type-safe branching.

Core ConceptsBeginner9 min readJul 10, 2026
Analogies

The match Expression

The match expression is F#'s primary tool for branching on the shape and value of data, replacing chains of if/elif with a single expression that examines a value against a series of patterns and evaluates the branch tied to the first pattern that matches. A simple example, "match x with | 0 -> \"zero\" | _ -> \"nonzero\"", checks whether x equals the literal 0 and falls through to the wildcard _ -- which matches anything -- otherwise.

🏏

Cricket analogy: An umpire reviewing a run-out via DRS checks a sequence of possibilities -- bat grounded, bails off, foot behind the line -- and rules on the first condition that clearly applies, much like match evaluates patterns in order and commits to the first one that fits.

Matching on Discriminated Unions

Pattern matching shows its full power against discriminated unions (DUs), where each case can carry different data and match both identifies which case a value is and extracts its payload in one step. Given "type Shape = Circle of float | Rectangle of float * float", the expression "match shape with | Circle r -> Math.PI * r * r | Rectangle (w, h) -> w * h" simultaneously checks the case tag and binds r or (w, h) for use in that branch's body.

🏏

Cricket analogy: Classifying a dismissal in cricket as Bowled, Caught, or LBW while simultaneously noting the specific fielder or delivery involved mirrors matching a DU case like Caught fielder, which identifies the case and extracts the fielder's name in one step.

fsharp
type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
    | Triangle of baseLen: float * height: float

let area shape =
    match shape with
    | Circle r -> System.Math.PI * r * r
    | Rectangle (w, h) -> w * h
    | Triangle (b, h) -> 0.5 * b * h

let describe n =
    match n with
    | x when x < 0 -> "negative"
    | 0 -> "zero"
    | x when x % 2 = 0 -> "positive even"
    | _ -> "positive odd"

printfn "%f" (area (Circle 3.0))
printfn "%s" (describe -4)

Exhaustiveness Checking and Wildcards

The F# compiler statically checks that a match expression covers every possible case of the value being matched, and emits a warning (FS0025) if any case is left unhandled, catching an entire class of bugs at compile time rather than at runtime. Adding a new case to a discriminated union will cause every existing match over that union to become non-exhaustive again, immediately flagging every call site that needs updating.

🏏

Cricket analogy: A pre-match checklist that must account for every possible weather outcome -- clear, rain delay, or bad light -- before the umpires proceed mirrors F#'s compiler refusing to let a match compile cleanly unless every DU case is handled.

A catch-all wildcard pattern _ silences the compiler's exhaustiveness check, which means adding a new case to a discriminated union later will NOT trigger a warning at any match site that already has a _ branch -- a common source of silently-wrong behavior after a union is extended.

Guards and Active Patterns

A when guard attaches an additional boolean condition to a pattern, letting you combine structural matching with arbitrary logic, as in "match n with | x when x < 0 -> \"negative\" | 0 -> \"zero\" | _ -> \"positive\"", where the first branch only fires if the bound value x also satisfies x < 0. Guards are evaluated only after the pattern itself matches, so they can reference any values already bound by that pattern.

🏏

Cricket analogy: A fielding restriction rule that only applies during the powerplay overs -- a condition layered on top of which over is being bowled -- mirrors a when guard adding an extra boolean check on top of a matched pattern.

Active patterns, declared with banana-shaped brackets like "let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd", let you define custom, named patterns that classify or decompose values in ways the built-in pattern syntax can't express directly, and they can then be used inside match exactly like a built-in case, e.g. "match n with | Even -> \"even\" | Odd -> \"odd\"".

🏏

Cricket analogy: A scoring analyst who defines a reusable custom classification like 'boundary over' (an over with 2+ fours or sixes) and applies it across every match report mirrors an active pattern like (|Even|Odd|) used repeatedly across match expressions.

Pattern matching isn't limited to discriminated unions -- you can destructure tuples (match point with | (0, 0) -> "origin" | (x, 0) -> "on x-axis" | _ -> "elsewhere") and lists (match xs with | [] -> "empty" | [x] -> "one item" | x :: rest -> "head and tail") directly in the same match syntax.

  • match compares a value against ordered patterns and runs the first branch that fits.
  • Matching a discriminated union both identifies the case and extracts its payload in one step.
  • The compiler performs exhaustiveness checking and warns (FS0025) about unhandled cases.
  • A when guard adds a boolean condition on top of a structural pattern.
  • Active patterns let you define custom, reusable classification logic usable inside match.
  • Tuples and lists can be destructured directly within match patterns.
  • A wildcard _ branch suppresses exhaustiveness warnings, which can hide bugs after a union changes.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#FStudyNotes#PatternMatchingInF#Pattern#Matching#Match#Expression#StudyNotes#SkillVeris#ExamPrep