What must be true of the ticket-state type before a compiler can prove a match over it is exhaustive?
answer
- something finite to subtract from
- all variants declared in one place
- no other module may add one
- unbounded domains cannot be enumerated
- judged on the static type at the match
basics
~20 sThe 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.
solid answer
~50 sExhaustiveness is a subtraction, so the compiler needs something finite to subtract from. The type must therefore declare its complete set of variants in one place and forbid anything else from adding one — a closed set, fixed when the program is compiled. A ticket state declared as exactly `New | Open | Waiting | Resolved` qualifies. A numeric priority or a free-text field does not: no finite list of patterns covers the domain, so a case that matches anything else is genuinely required rather than lazy. Two further conditions are easy to miss. The check runs against the **static type at the match site**, so widening the value to something more general before matching hands the compiler a different, larger set to enumerate. And a case whose pattern is conditioned on a run-time test cannot be counted as covering its variant, because the compiler cannot prove the test will succeed.
code
pseudocode · 11 linestype TicketState = New | Open | Waiting | Resolved
function label(state)
match state
case New -> "just filed"
case Open -> "being worked"
case Waiting -> "waiting on the reporter"
case Resolved -> "closed out"
// the four variants live in one declaration and nothing else may add a fifth,
// so the compiler can enumerate them and prove this match covers them allgo deeper
Remember the precondition: the compiler can only check a match when the type states up front exactly which variants exist and nothing elsewhere may add another one.
Explain why the check is a subtraction, and name the cases where there is nothing finite to subtract from — an extensible type, or a whole numeric or text domain.
Watch for the quiet loss: a value widened to a more general type before the match hands the compiler a different variant set from the one you had in mind.
Judge where a closed type earns its cost. Inside one codebase it is nearly free; a type that crosses a component boundary makes every added variant a coordinated release.
## Exhaustiveness is a subtraction The check a compiler performs is arithmetic on sets: take the variants the matched type can hold, remove every variant some listed pattern covers, and demand that the remainder be empty. Everything a type must provide follows from that one sentence. There has to be a **set**, it has to be **finite**, and it has to be **known to the compiler at the match site**. A type that supplies all three is called **closed**. ## What "closed" means in practice - The **complete list of variants is declared in one place**, alongside the type itself, rather than accumulated from wherever someone happened to add one. - **Nothing outside that declaration may add a variant.** If another module — or another team's library, compiled separately — could introduce a fifth ticket state, no match anywhere could ever be proved complete, because the compiler would be reasoning about a set it cannot see all of. - The set is fixed **when the program is compiled**, not when it runs. Loading something later that claims to be a new state does not extend it; a value must be one of the declared variants to be a value of that type at all. - Being closed says nothing about how *many* variants there are. Four is closed, forty is closed. Finiteness, not smallness, is the requirement. The pay-off is that the compiler can answer a question about the *whole program* from one declaration: "which shapes can this value have?" No analysis of who constructs the value, and no scan of call sites, is involved. ## Domains with nothing enumerable in them Not every value has a variant set. A ticket's priority as a number, its title as text, or its identifier as an opaque key all live in domains far too large to list. A match on three specific numbers leaves an enormous uncovered remainder, and no amount of typing shrinks it to nothing, so such a match needs a case that matches whatever is left. That case is not a lapse of discipline — it is the only way to be total over a domain the compiler cannot enumerate, and it is a good reason to model a lifecycle as a closed set of variants rather than as a number or a string in the first place. | What is being matched | Can it be proved exhaustive without an all-matching case? | |---|---| | A closed set of four ticket states | Yes — the remainder is computable and can be driven to empty | | A type that any module may extend with new variants | No — there is no complete set to enumerate | | A numeric priority over an unbounded range | No — a finite list of literals always leaves a remainder | | A pair of two values from closed sets | Yes — completeness is judged over the combinations | | Text read from storage, before it is decoded | No — it is not yet a variant of anything | ## The static type at the match site A subtle failure: the analysis uses the type the value is **declared** to have where the match appears, not the type it was originally constructed as. If a ticket state is widened to some broader type before being matched — passed through a parameter or a field declared more generally — the compiler enumerates the variants of *that* type. The set may be larger, or may not be closed at all, and a match that felt complete stops being provable. The habit worth forming is to keep the closed type all the way to the match rather than losing it somewhere in the middle of the call chain. ## Nested values and conditioned cases Two refinements come up as soon as the patterns get more interesting than one variant name: 1. **Nested patterns need nested closure.** Matching a pair of ticket states means the compiler is enumerating combinations — with four variants on each side, sixteen of them. A pattern that fixes one side and leaves the other unconstrained covers the four combinations underneath it. If either side is drawn from an open or unbounded domain, the pair cannot be proved complete however the outer patterns are written. 2. **A case conditioned on a run-time test does not count as coverage.** If a case matches a variant only when some extra condition holds, the compiler has no way to prove the condition holds, so the variant is still treated as potentially uncovered. That is not a defect in the check — it is the check declining to assert something it cannot prove. ## Why the precondition is the interview question Candidates who have only used the feature describe the error message. Candidates who understand it describe the property that makes the error message possible, and can therefore predict when it will not appear: an extensible type, a widened value, an unbounded domain, a guarded case. That prediction is the difference between relying on the compiler and hoping for it.
- Does a closed variant set have to be small for the check to be useful?No — only finite and fixed. The analysis is as happy with forty variants as with four. What does not scale is human patience, and a large set is exactly where someone adds a case matching everything rather than typing the rest. That is how the guarantee is usually lost on big types, not through any limit in the compiler.
- Two ticket states are matched together as a pair — what does the compiler enumerate then?The combinations. With four variants on each side, completeness is judged over the sixteen pairs, and a pattern that fixes one side while leaving the other unconstrained covers the four combinations beneath it. Both sides must be closed: if either is drawn from an open or unbounded domain, the pair cannot be proved complete.
saying these in an interview costs you the question
- Thinks any type with a fixed list of names is automatically closed
- Believes a match over every possible number can be made exhaustive
- Assumes widening the value before matching keeps the same variant list
- Says the compiler finds the variants by scanning the whole program
- Treats a case conditioned on a run-time test as full coverage