In a match tried top to bottom, why can a broader case placed first make a later, narrower case unreachable?
answer
- first match, not best match
- order is precedence
- each case is a set of values
- subset below superset is dead
- wildcards widen more than they look
basics
~20 sMatching 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.
solid answer
~40 sCases are tried in order and the first one that accepts the value wins, so order is precedence, not decoration. Think of each case as the set of values it admits. If a later case's set is contained in an earlier, unguarded case's set, every value it was written for is taken above it and the case is dead code - `case Booking(_, Range(_, _))` placed first swallows a same-day booking before a `where end == start` case below can see it. The fix is ordering, not rewriting: narrower cases first, the general one last. A guard changes the picture, because a guarded broad case only takes the values its guard admits and leaves the rest to fall through.
code
pseudocode · 5 linesmatch request
case Booking(_, Range(_, _))
return nightlyQuote(request)
case Booking(_, Range(start, end)) where end == start
return sameDayQuote(request)go deeper
Hold on to the rule itself: the first case that fits wins and the rest are never tried. A case written below one that already accepts its values can never run.
Reason out loud in terms of which values each case admits, and name the wildcard as the thing that quietly widens a case. Then give the fix: narrower case above the one that contains it.
Show that you treat reordering as a behaviour change - guards hide overlap from structural checks, so a silent build is not evidence, and each branch deserves a test that pins it.
The standard worth setting is where the default case lives and how special cases are added, so that appending a case at the bottom of a long ladder is not the default way new behaviour gets written.
## First match wins A match is a ladder. The value is offered to each case in written order, and the **first** case that accepts it - pattern fits, guard (if any) holds - is the one that runs. Nothing below is consulted. There is no scoring, no "most specific wins", no backtracking to reconsider a better fit. That single rule turns the order of the cases into part of the program's meaning. Two matches with identical cases in different orders are different programs. ## Cases as sets of values The useful way to reason about overlap is to read each case as the **set of values it admits**. - `Booking(_, Range(_, _))` admits every booking whose stay is a two-ended range. - `Booking(_, Range(start, end)) where end == start` admits the same-day subset of those. - `Booking(_, _)` admits every booking at all. Now the reachability rule states itself: a case is **unreachable** when every value it admits is already admitted by some earlier case. In set terms, its set is a subset of the union of the sets above it. The classic instance is the narrow case placed below the broad one it refines - its set is a strict subset of the case directly above, so not one value ever reaches it. ## Why a wildcard makes this so easy to do by accident A wildcard position admits everything, so each wildcard **widens** the case's set. A pattern written with wildcards in the positions the author did not care about is often far broader than it looks on the page, because the eye reads the constructor name and skips the underscores. `Booking(_, Range(_, _))` reads as "a ranged booking" and behaves as "every ranged booking, including every special case of one". Placing it above anything that refines a ranged booking kills that refinement silently. ## Guards change what is shadowed A guarded case only takes the values its guard admits; the rest fall through and can be handled below. So: | First case | Second case | Second reachable? | |---|---|---| | Broad pattern, no guard | Narrower pattern | No - the first takes everything | | Broad pattern with a guard | Same broad pattern, no guard | Yes - for the values the guard rejected | | Narrow pattern | Broad pattern | Yes - for everything the narrow one missed | | Two disjoint patterns | - | Order is irrelevant either way | This is why the conventional shape of a ladder is *special cases first, with their guards; general case last, unguarded*. It reads top to bottom as a list of exceptions followed by a default, and each case only has to state what makes it special, because everything above it has already declined the value. ## Will something tell me? Often, not always. Where the cases overlap **structurally**, many checkers can see that a case is dead and report it. Where the overlap involves a **guard**, the condition is ordinary code, so the tools generally cannot decide whether it will ever be false and stay quiet. Treat a warning as a bonus rather than a safety net: reordering cases is a behaviour change that can pass a build unremarked. ## What this costs in practice 1. **Reordering is a semantic edit.** Sorting cases alphabetically, or moving the "simple" case to the top while reading, can change results without changing a single character inside any case. 2. **Dead cases mislead readers.** An unreachable case still documents an intent, so the next reader believes a situation is handled that never is. 3. **New cases land in the wrong place.** Appending a case at the bottom is the safest-feeling edit and the one most likely to be dead, because the bottom is where the broad default already sits. ## What an interviewer is listening for - The rule stated flatly: **first match wins**, not best match. - Reasoning in terms of which values each case admits, rather than an informal notion of specificity. - The recognition that wildcards widen a case more than they look like they do. - The nuance about guards: a guarded broad case shadows only its guard's values, which is exactly why guarded special cases go on top.
- Does a broad case with a guard also shadow every case below it?No. It takes only the values its guard admits and lets the rest fall through, so cases below it stay reachable for exactly those rejected values. That is why the usual ladder puts guarded special cases on top and the unguarded general case last.
- How do you tell whether one case can shadow another?Compare the sets of values each pattern admits. If the later case's set is contained in an earlier unguarded case's set, it is dead. If the sets are disjoint, order between them is irrelevant. If they merely overlap, order decides only the shared values.
- Will a build reliably tell you that a case is unreachable?Not reliably. Purely structural overlap is often reported, but when the overlap depends on a guard the condition is ordinary code, so tools usually cannot prove the case is dead and say nothing. Treat reordering as a behaviour change and test it.
saying these in an interview costs you the question
- Thinks the most specific case wins regardless of where it sits
- Believes every case is tried and the best match is selected
- Assumes a broad case with a guard shadows all later cases
- Thinks an unreachable case is always reported by the build
- Reorders cases assuming the match is order-independent
- Reads a wildcard position as narrow because it looks empty