Exhaustive
A match is exhaustive when every value it could meet has an arm. You have already seen the compiler insist on it — a missing case in Variant match, a missing else in Match expression. This lesson collects the rules in one place and, more importantly, explains why the check is a feature you want: it is what lets a program grow without silently breaking.
A closed list of cases
A variant or an enum has a closed list of cases, so the compiler knows every value a match on it could meet, and it insists that every one has an arm:
variant Player {
Stopped,
Playing(int32),
Paused(int32)
}
func Describe(player: Player) -> char8[..] {
return match player {
.Stopped => "stopped",
.Playing(second) if second < 5 => "just started",
.Playing(_) => "playing",
.Paused(_) => "paused"
};
}
Every case has an arm, so no else is needed — and none should be written. Notice the two .Playing arms. A guarded arm does not count towards covering its case, because the guard might be false; .Playing(second) still needs the unguarded .Playing(_) below it.
Why the check helps
That check is what makes a variant safe to grow. Suppose you add a Buffering case to Player next year:
flowchart LR
add["Add a Buffering case"] --> q{"How does each<br/>match end?"}
q -- "names every case" --> err["Fails to build: missing<br/>Player::Buffering,<br/>pointing at the spot to fix"]
q -- "ends with else" --> quiet["Builds silently:<br/>Buffering takes the<br/>else arm's answer"]Every match that names its cases one by one fails to compile, and the error points at the exact place to decide what buffering means. A match with else keeps compiling — and quietly gives Buffering whatever answer else gives.
else on a variant is a promise
That is the trade-off in IsPlaying:
func IsPlaying(player: Player) -> bool {
return match player {
.Playing(_) => true,
else => false
};
}
An else on a variant is allowed, but it is a promise you make on behalf of cases that do not exist yet: a future Buffering would count as "not playing" with no error to warn you. Use it only when that is truly the right answer for any new case.
else is for the values you cannot list
An integer has billions of values, so a match that produces a value from an integer must end with else, and deciding what the leftovers mean is up to you:
func Loudness(volume: int32) -> char8[..] {
return match volume {
0 => "muted",
1..=3 => "quiet",
10 => "maximum",
else => "normal"
};
}
Even ranges that happened to reach every value would not do instead: an integer match producing a value needs its else.
| Matched type | Covered by | else |
|---|---|---|
| A variant or an enum | one unguarded arm per case | allowed, but hides new cases |
bool | a true arm and a false arm | not needed |
A tuple of bools | every combination | not needed |
| An integer | nothing short of else | required to produce a value |
A match used as a statement, producing no value, is more relaxed about integers and bools: a value with no arm simply skips the match, as in Match. A match on a variant must still name every case.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A variant or an enum has a closed list of cases, so the compiler knows every value a match on it
// could meet, and it insists that every one has an arm. Leave a case out and the program does not
// build. Delete the `.Paused` arm below and the compiler names what is missing:
// error: match on 'Player' is not exhaustive; missing Player::Paused
//
// That check is what makes a variant safe to grow. Add a case such as `Buffering` to `Player` next
// year, and every match that forgot it fails to compile, pointing at the exact spot to fix. A match
// that names every case therefore has no `else`, and should not have one.
//
// `else` is for the values you cannot list. An integer has billions of them, so a match that
// produces a value from an integer must end with `else`, and deciding what the leftovers mean is
// up to you. Delete the `else` arm in `Loudness` and the compiler stops with
// error: match on 'int32' is not exhaustive; its arms do not cover every value
// A `bool` has just two values, so `true` and `false` arms cover it, and a missing one is named:
// error: match on 'bool8' is not exhaustive; missing false
// A match used as a statement, producing no value, may leave integers and bools out: a value with
// no arm simply skips it.
import Io::PrintLine;
variant Player {
Stopped,
Playing(int32),
Paused(int32)
}
// Every case has an arm, so no `else` is needed. A guarded arm does not count towards that: the
// guard might be false, so `.Playing(second)` still needs an unguarded arm of its own. Without it
// the error would read `missing Player::Playing`.
func Describe(player: Player) -> char8[..] {
return match player {
.Stopped => "stopped",
.Playing(second) if second < 5 => "just started",
.Playing(_) => "playing",
.Paused(_) => "paused"
};
}
// An `else` on a variant is allowed, but it is a promise you make on behalf of cases that do not
// exist yet: a future `Buffering` would quietly count as "not playing" here, with no error to
// warn you. Use it only when that is truly the right answer for any new case.
func IsPlaying(player: Player) -> bool {
return match player {
.Playing(_) => true,
else => false
};
}
// The arms name a few volume levels, and `else` covers the rest. Even ranges that happened to reach
// every value would not do instead: an integer match producing a value needs its `else`.
func Loudness(volume: int32) -> char8[..] {
return match volume {
0 => "muted",
1..=3 => "quiet",
10 => "maximum",
else => "normal"
};
}
func Main() -> int {
let states: Player[4] = [
Player::Stopped,
Player::Playing(2),
Player::Playing(95),
Player::Paused(95)
];
for index in 0..4 {
PrintLine("{:12} playing: {}", Describe(states[index]), IsPlaying(states[index]));
}
PrintLine("0 {}, 2 {}, 7 {}, 10 {}", Loudness(0), Loudness(2), Loudness(7), Loudness(10));
return 0;
}
Run it
cd Examples/Patterns/Exhaustive
rux run
stopped playing: false
just started playing: true
playing playing: true
paused playing: false
0 muted, 2 quiet, 7 normal, 10 maximum
Common mistakes
Delete the
.Paused arm and the program does not build: error: match on 'Player' is not exhaustive; missing Player::Paused. The message names the case, so the fix is to add its arm — not an else.Delete
.Playing(_) => "playing", and the guarded .Playing(second) if second < 5 arm is left alone. It might not match, so the error reads missing Player::Playing.else.Delete the
else arm in Loudness and the compiler stops with error: match on 'int32' is not exhaustive; its arms do not cover every value. A bool match is held to the same rule, but there two arms are enough; leave one out and it is named: error: match on 'bool8' is not exhaustive; missing false.Try it yourself
- Add a
Bufferingcase toPlayerand runrux check. Which function fails, and which one keeps compiling? - Rewrite
IsPlayingwithoutelse, so that a future case is reported instead of quietly counted as "not playing". - Give
Loudnessarms that between them cover0..=10, then delete theelse. Read why it still does not compile.
Learn more
matchand enumerations in the Rux Reference- Enum and Variant match — the closed lists that make the check possible
- Presence — the same rule for optionals, where the case you cannot forget is
none