skip to content

Pattern Matching

Algebraic data types plus match expressions that bind a value's parts by shape and are checked for completeness. Interviewers use exhaustiveness to see if you make illegal states unrepresentable.

on this pageshow

questions

16

In a match expression, what does binding a value's parts inside the pattern give you that reading fields in the branch body does not?

level: juniorimportance: must knowfreq 62%

answer

  1. one construct, two jobs
  2. test the shape, name the parts
  3. bindings arrive already in scope
  4. nested pattern reaches through a layer
  5. wildcard matches, binds no name

basics

~20 s

Binding in the pattern makes one construct do two jobs: it tests that the value has the case's shape and names the parts of that shape at the same time. The branch body starts with named parts instead of shape checks and accessor calls.

solid answer

~40 s

A pattern is the shape a case is written for. Matching a value against it tests whether the value has that shape and, when it does, binds each hole in the shape to a name the case body can use. `case Booking(guest, Range(start, end))` selects only a booking whose stay is a two-ended range, and inside that case `guest`, `start` and `end` already exist. Doing it in the body instead means checking the shape yourself, then pulling parts out with accessors, and usually re-asserting a shape the check already established. Patterns also nest, so one pattern reaches through a booking into its stay; the body never names the intermediate value it does not care about. A wildcard fills a position you are not asking about and binds nothing.

code

pseudocode · 7 lines
pseudocode
function quote(request)
    if isBooking(request) and isRange(request.stay)
        guest = request.guest
        start = request.stay.start
        end   = request.stay.end
        return nightlyQuote(guest, start, end)
    return fallbackQuote(request)

go deeper

for a junior

Be able to say the two jobs in one breath: the pattern decides whether the case applies, and it names the parts that case needs. Then show a two-level pattern reaching into a nested value.

for a middle

Explain what the body-side alternative costs: a hand-written shape test per layer that can drift from the extraction beside it, and intermediate values the case never really wanted.

for a senior

Show the judgment about readability: a wildcard where a part is irrelevant, names only where they are used, and cases whose first line tells a reviewer exactly which situation is being handled.

for a principal

Frame it as a house convention. Deciding that shape decisions are taken in patterns rather than in branch bodies is what keeps the check and the extraction from diverging as a codebase grows.

## One construct, two jobs A **pattern** describes the shape a case is written for, and matching a value against it does two things in the same step. 1. It **tests**: does this value have that shape? A booking whose stay is a two-ended date range matches `Booking(guest, Range(start, end))`; a booking whose stay is open-ended does not. 2. It **binds**: every hole in the shape becomes a local name holding the part that filled it. Inside the case, `guest`, `start` and `end` are already in scope. That is the whole of what "destructuring in the pattern" means. The alternative - test the shape, then extract fields with accessors in the body - splits those two jobs apart, and every difference below follows from that split. ## Nesting reaches through a layer Patterns compose the way the data composes. A booking is a **product**: it holds a guest *and* a stay. A stay is a **sum**: it is *either* a two-ended range *or* an open-ended start. A nested pattern walks both in one expression. - `Booking(guest, Range(start, end))` matches a booking whose stay is a range, naming three parts at two different depths. - `Booking(_, OpenEnded(start))` matches the other stay shape and names only the start. - `Booking(guest, _)` matches any booking at all and names only the guest. The depth of the pattern is the depth of the reach. The body never has to hold the stay in a variable so it can ask it a second question, which is why a destructured case usually reads as one sentence about the one situation it handles. ## A wildcard is a part you are not asking about A **wildcard** matches whatever sits in its position and binds nothing. It is not a name: nothing in the body can read it. Two consequences are worth saying out loud. - It still occupies a **position**, so the pattern's shape is unchanged - `Booking(_, _)` is still a two-part booking, just one whose parts this case ignores. - Writing a wildcard rather than an unused name states the intent directly: this part does not participate in this case, so no reader goes hunting for where it is used. ## Scope of what a pattern binds The names a pattern binds belong to **that case only**. They come into existence when that case is selected and are not visible in the other cases or after the match finishes. Two cases may bind the same name from different positions without colliding, because they are different bindings in different scopes. This is why reading a destructured match is local work: the meaning of `start` is fixed by the one pattern directly above it. ## The body-side version, and what it costs | Concern | Bound in the pattern | Extracted in the body | |---|---|---| | Shape test | Part of the case; the case is chosen only if the shape fits | A separate condition the author must write and keep correct | | Naming the parts | Automatic, at the moment the test succeeds | Manual accessor calls after the test | | Nested shapes | One pattern reaches every depth | An intermediate value per layer, each usually re-tested | | When the shape does not fit | Control moves on to the next case | The author must remember to fall through or return | | Reading the code | The situation handled is visible on the case line | The situation is spread over the first few lines of the body | The practical failure of the body-side version is the third and fourth rows. Each extra layer adds a test the author can forget, and a test written by hand can disagree with the extraction that follows it - the shape is checked one way and taken apart another. A pattern cannot drift from itself, because the test and the extraction are the same piece of text. ## What an interviewer is listening for - That you say **both** jobs, not just "it checks the type". Candidates who describe patterns as type tests alone usually go on to re-extract in the body. - That you know patterns **nest**, and can show a two-level pattern rather than a match inside a match. - That a **wildcard binds nothing** and is a statement about relevance, not a placeholder name. - That bindings are **local to their case**. This comes up the moment someone tries to use a name after the match. None of this is about a particular syntax. Where languages differ is in the spelling and in how much of a value's structure is available to a pattern at all; what does not differ is the idea that the shape test and the names it produces are one construct.

  • What does a wildcard in a nested position of a pattern do?
    It matches whatever occupies that position and binds no name, so the case is selected without the body gaining access to that part. `Booking(guest, Range(_, end))` handles any range, cares about the end date, and says plainly that the start is irrelevant here.
  • Are the names a pattern binds visible outside the case that bound them?
    No. Each case's bindings live only in that case, so two cases can bind the same name from different positions without interfering, and nothing after the match can read either. That locality is why a reader can interpret a case from its own pattern line.
  • Is a pattern restricted to naming parts, or can it also demand specific values?
    It can demand values too: a position may hold a constant instead of a name, so the case matches only when that part equals the constant. Naming a part and constraining it are the same mechanism at different strengths, which is why one pattern can express both.

A pattern is a cutting template laid over the value: if it fits, the pieces come out already labelled. Checking in the body is measuring the sheet first and then cutting freehand.

saying these in an interview costs you the question

  • Thinks a pattern only tests the shape and never binds anything
  • Re-checks in the branch body the shape the pattern already established
  • Believes patterns cannot nest, so every layer needs its own match
  • Treats a wildcard as a name the body can read later
  • Assumes a name bound in one case is visible in the other cases
open as a page

What does a compiler's exhaustiveness check on a match over ticket states actually guarantee?

level: juniorimportance: must knowfreq 62%

basics

~20 s

An exhaustiveness check proves at compile time that a match handles every variant its type can hold. The payoff comes later: add a variant and every match with a gap fails the build instead of falling through at run time.

open as a page

In algebraic data types, what distinguishes a payment that is exactly one of card, transfer or credit from a record holding all three?

level: juniorimportance: must knowfreq 72%

basics

~20 s

A sum type holds exactly one of its variants, tagged so you can tell which; a product type holds a value of every component at once. The payment is a sum, the record of all three is a product.

open as a page

When a match case's pattern fits but its guard condition evaluates to false, what does the match do next?

level: middleimportance: must knowfreq 55%

basics

~20 s

The case is rejected as a whole and matching continues with the cases below it, exactly as if the pattern had not fitted. A failed guard is not an error and does not abort the match.

open as a page

A match over ticket states ends with a catch-all case — what does that cost when a new state is added?

level: middleimportance: must knowfreq 66%

basics

~20 s

A catch-all makes the match total for every variant, including ones that do not exist yet, so the compiler reports nothing when a state is added. The new state silently takes a branch written for the variants of a year ago.

open as a page

An account record pairs a boolean active flag with a nullable credential field — what does replacing it with two variants prevent?

level: middleimportance: must knowfreq 62%

basics

~20 s

It prevents the two fields from disagreeing. Two variants — invited carrying no credential, active carrying one — give the contradictory pairings no constructor at all, so no reader has to check the credential's presence and no code path can create a half-active account.

open as a page

Why does modelling payment as three variants beat one record with six optional fields for a reader?

level: middleimportance: must knowfreq 58%

basics

~20 s

Six optional fields admit sixty-four present-or-absent combinations, of which three are meaningful; three variants admit exactly three. The closed choice states which fields travel together, instead of leaving every reader to reconstruct that rule from prose or from other code.

open as a page

An account record holds a boolean active flag plus an optional credential — how many states can it hold, and how many are legal?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Four representable states, two legal. Counting the credential only as present or absent, the two independent fields multiply to 2 × 2 = 4 combinations, and the domain allows only invited-without-credential and active-with-credential — the other two were never meant to exist.

open as a page

In a match tried top to bottom, why can a broader case placed first make a later, narrower case unreachable?

level: middleimportance: should knowfreq 50%

basics

~20 s

Matching is first-match: the first case whose pattern fits and whose guard holds wins, and the rest are never tried. A case whose values are all admitted by an earlier case can therefore never be reached, whatever it says.

open as a page

What must be true of the ticket-state type before a compiler can prove a match over it is exhaustive?

level: middleimportance: should knowfreq 44%

basics

~20 s

The variant set must be closed and fixed at compile time: declared in one place, with no way for other code to add a variant later. Only a finite, known set gives the compiler something to subtract the listed patterns from.

open as a page

A check on an account returns a bare boolean — what information has that return type thrown away?

level: middleimportance: should knowfreq 42%

basics

~20 s

Everything except one bit. The boolean says a check succeeded but not what was checked, what it produced, or why it failed — so the caller must re-derive all of it, and nothing stops the two bits from being confused at the call site.

open as a page

A booking match gives the right answer only because two overlapping cases sit in a particular order - how do you remove that dependency?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Make the cases disjoint, so each value is admitted by exactly one of them: push the distinguishing condition into both cases, typically by giving the broader case the negation of the first one's guard. Then reordering the pair changes nothing.

open as a page

Adding a ticket state breaks the build at every match site — how do you sequence that change across a large codebase?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Add the variant first with nothing producing it, then work the compiler's error list site by site. That list is complete only for matches that never had a catch-all, so audit existing catch-alls and every decoder of stored values separately.

open as a page

Which account rules can the type itself make unrepresentable, and which still need a check at one construction point?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Rules about which fields coexist can be encoded as variants; rules needing values the type does not hold — a format, a comparison against other records, anything time-dependent — cannot. Those move to one construction point that returns the strict value or a failure.

open as a page

Your service must expose a closed three-variant payment type across an interface that carries only records of optional fields — what do you lose, and how do you keep the choice recoverable?

level: seniorimportance: should knowfreq 40%

basics

~20 s

You lose the guarantee that the shape carries: the flattened record permits combinations the closed choice cannot express. Carry an explicit tag field naming the variant, rebuild the choice on the way in, and treat the flat record as a transport shape only.

open as a page

A pair of a three-variant payment type and a boolean flag — how many distinct values can it take?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

Six. A pair is a product, so its value count is the product of its components' counts: three payment variants times two flag values. Multiplication for a record and addition for a choice are the two operations the word algebraic names.

open as a page