Explainstuff.mebeta
All concepts
Functional Programmingbeginner6 min

Pattern Matching

A supercharged switch that branches on the shape of a value, unpacks it in the same breath, and nags you if you forget a case.

Imagine a long chain of questions about a single value: "Is it empty? No. Does it have a name? Yes, but is the name blank? And is it a circle or a rectangle? And if it's a circle, what's its radius?" Written out as if / else if / else if ..., this quickly turns into a tangled ladder that's hard to read and dangerously easy to get wrong — miss one rung and your program quietly does the wrong thing.

Pattern matching is the cleanup. It's a supercharged switch statement that looks at the shape and contents of a value and routes it to exactly one matching branch — clearly, all in one place.

How it works

You hand one value to a match ... with expression, which lists several cases, each describing a shape the value might have. The value is checked against the cases from top to bottom, and the first one that fits wins — its branch runs, and the rest are skipped.

The clever part is that a case can also unpack the value as it matches, lifting the pieces you care about out into named variables. So matching and grabbing-the-contents happen in a single, tidy step.

Route to the matching case
match
value
match
Case A
Case B
Case C
match sends the value to exactly one branch — and the compiler warns if you miss a case.

The classic place this clicks is the maybe-value — an Option that's either Some x (a value is present) or None (nothing's there). Matching makes you face both possibilities head-on:

let describe (opt: int option) =
    match opt with
    | Some x -> sprintf "Got the number %d" x
    | None   -> "Got nothing at all"

describe (Some 42)   // "Got the number 42"
describe None        // "Got nothing at all"

Look at Some x: it both checks "is there a value?" and binds that value to x so the branch can use it. No null checks, no peeking inside — the shape and the contents come out together.

Tip

This is where exhaustiveness earns its keep. If you'd written only the Some x case and forgotten None, the F# compiler would warn you: "Incomplete pattern match — what about None?" The bug that would've crashed at runtime gets caught before you even run the program.

Miss a case and the compiler catches it
compiler: case missing!
Shape
match
Circle ✓
Rectangle ✓
Triangle — unhandled
Circle and Rectangle are handled, but Triangle isn't — exhaustiveness checking turns a runtime surprise into a compile-time warning.

Matching on your own shapes

Pattern matching really comes alive next to a discriminated union — a type that says "a value of this is one of these cases." Here's a Shape that's either a circle or a rectangle, each carrying its own measurements:

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

let area shape =
    match shape with
    | Circle r          -> 3.14159 * r * r
    | Rectangle (w, h)  -> w * h

area (Circle 2.0)          // 12.566...
area (Rectangle (3.0, 4.0)) // 12.0

Each case names the shape and pulls out exactly the numbers that shape carries — r for the circle, w and h for the rectangle. Add a third shape like Triangle later, and the compiler will point right at this match and remind you it now needs a triangle case too.

Sometimes a case needs an extra condition — not just "is it a rectangle?" but "is it a rectangle that happens to be a square?" That's what a guard is for, using the when keyword:

let label shape =
    match shape with
    | Rectangle (w, h) when w = h -> "A square!"
    | Rectangle _                 -> "A rectangle"
    | Circle _                    -> "A circle"

The when w = h clause runs only if the value already matched the shape and passes the test. The _ you see is a wildcard — it means "there's a value here, but I don't care what it is."

Why it's great

Compared to an if / else if ladder, a match reads like a labelled menu: every case a value could be is right there, side by side, instead of buried in nested branches. You spend less effort following the logic and more confidence that you've covered everything.

And that confidence isn't just a feeling — it's enforced. Because the compiler checks for exhaustiveness, "oops, I forgot to handle that case" stops being a 2 a.m. production mystery and becomes a friendly squiggle in your editor.

Note

Pattern matching and discriminated unions are a team. The union says "this data is exactly one of these cases," and matching is how you safely take it apart. Together they let you model your problem honestly — and then have the compiler make sure you've dealt with every possibility.

So next time you feel an if / else if ladder growing rung by rung, reach for match ... with instead. Branch on the shape, unpack the contents in the same line, lean on guards for the fiddly cases, and let exhaustiveness watch your back.

Key takeaways

  • Pattern matching (`match ... with`) branches on the shape and contents of a value, not just whether two things are equal — it's a switch with x-ray vision.
  • In the same step it can destructure a value, pulling the pieces you care about straight out into named variables.
  • It pairs perfectly with the maybe-value: `match opt with Some x -> ... | None -> ...` forces you to handle both the present and the missing case.
  • It shines on discriminated unions that model "one of these cases" data, like a Shape that's either a Circle or a Rectangle.
  • Best of all, exhaustiveness: the compiler warns you when you've missed a case, turning a whole class of forgot-to-handle-that bugs into compile errors.

Keep going