Reading a Value — exhaustive match
Every time the engine acts on a cell (coercing it, comparing it, formatting it for an error)
it first has to ask which of the nine shapes is this Value? and branch on the answer.
Do that in dozens of places across the engine and one question becomes load-bearing: when a
tenth shape is added, what stops a single one of those branches from silently going stale?
You met the closed Value enum in the Value cell as “one of nine
shapes, fixed at compile time.” A closed set is only half the deal; the other half is the
tool that takes a Value apart, the Rust match, and its defining property: the compiler
refuses to let you forget a case. We use just enough of match to read the engine’s real
dispatch code, and The Rust Book, ch. 6
is the canonical treatment if you want pattern matching from first principles.
What you’ll be able to do
Section titled “What you’ll be able to do”- Read a
matchoverValueand name what each arm does, including what(_)and a named binding capture. - Distinguish an exhaustive
matchfrom one that leaks a case, and predict which one the compiler rejects. - Write a
matchthat handles every variant of a small closed enum, scaffolded down to an unaided task. - Explain why exhaustiveness turns “add a tenth shape” from a hunt-for-every-site nightmare into a compiler-guided chore, and why a
_ =>catch-all forfeits that.
New terms in this lesson (each is also expanded inline at the point you first need it):
match: pick a branch by which variant the value is.- arm: one
pattern => expressionbranch of amatch. - exhaustiveness: the rule that every variant must be covered, enforced at compile time.
- binding: naming a variant’s payload so the arm can use it;
(_)matches it but keeps nothing.
Predict first
Section titled “Predict first”A real match: type_name
Section titled “A real match: type_name”The engine constantly needs to ask a Value “what shape are you?”, whether for diagnostics,
coercion, or error messages. Value::type_name answers it with one
match
over all nine variants:
clinker-record ·value.rs ·type_name fn @19acdcb4
pub fn type_name(&self) -> &'static str { match self { Value::Null => "null", Value::Bool(_) => "bool", Value::Integer(_) => "int", Value::Float(_) => "float", Value::String(_) => "string", // ... one arm per variant, all nine }}Each line is one
arm:
a pattern on the left of =>, the value to return on the right. Value::Null matches the
unit variant. Value::Bool(_) matches the Bool variant, and its
(_) says “I don’t care about the payload, just the shape”: it’s a
binding
that captures nothing. If this function needed the inner number instead, it would write
Value::Integer(n) => /* use n */ and the n would be the i64 itself.
Why exhaustiveness is the point
Section titled “Why exhaustiveness is the point”A match over a closed enum must cover every variant: leave one out and the code won’t
compile. That sounds like nagging until you imagine the alternative. Value is dispatched on
across the whole engine: coercion, comparison, serialization, formatting. Now suppose a tenth
shape is added. With an exhaustive match, the compiler immediately lists every single
site that must be updated. Miss none, ship nothing half-handled. Without it, you’d be
grepping and praying.
This
exhaustiveness
is the deep reason the closed enum from the last lesson pays off: closed set + exhaustive
match = the compiler is a complete, always-current checklist of “have you handled every
case?” (It’s also why reaching for a _ => catch-all is a smell in this codebase: it
silences exactly the warning you want.)
Break it on purpose
Section titled “Break it on purpose”> output appears here — press Run
It compiles and prints three type names. Now delete the Value::Float arm and Run. The
compiler stops you with an error, non-exhaustive patterns: &Value::Float(_) not covered,
naming the exact case you dropped. That is the prediction from the top, on a value you can
run.
Worked → completion → faded
Section titled “Worked → completion → faded”You’ve read the real match and watched it refuse an incomplete one. Now you write the
machinery, scaffolded down to an unaided task. Each rung adds exactly one idea.
Worked: read every arm, bind one payload
Section titled “Worked: read every arm, bind one payload”Here is a small, self-contained match that mirrors type_name’s structure, but one arm
goes further and binds its payload to use it. Run it as given. Null and Bool(_) match
the shape and ignore the payload; Integer(n) and Text(s) bind the inner value so the
body can read it.
> output appears here — press Run
Completion: add the missing arm
Section titled “Completion: add the missing arm”Below, the Value enum has a Float variant, but type_name is missing the arm that
handles it. Three arms are written for you. Add the one Value::Float(_) arm. Until you do,
the compiler refuses to build with a non-exhaustive patterns error pointing at exactly the
gap.
> output appears here — press Run
💡 Hint 1
Value::Float(_) => "float",. The (_) matches the f64 payload without binding it, because type_name only needs the shape. Resist adding a _ => ... catch-all; spell the variant out so a future tenth shape still breaks the build.Show solution
fn type_name(v: &Value) -> &'static str { match v { Value::Null => "null", Value::Integer(_) => "int", Value::Float(_) => "float", }}With every variant spelled out, adding a fourth variant to Value would break this build at
exactly this match, which is the behavior you want. A _ => "unknown" catch-all would
have compiled today and silently mishandled that future variant. The exhaustive match is
what guarantees you considered all the cases; the catch-all is what throws that guarantee
away.
Faded: write the whole match
Section titled “Faded: write the whole match”Now with much less scaffolding. Write numeric_or_zero, which returns the inner number for
the numeric variants and 0 for everything else. The rule: Integer(n) returns n;
Float(f) returns f rounded to an i64 with f as i64; Null and Bool both return
0. You must bind the payload in the numeric arms (Integer(n), Float(f)) to return
it. Spell out every variant, no catch-all.
> output appears here — press Run
Show solution
fn numeric_or_zero(v: &Value) -> i64 { match v { Value::Integer(n) => *n, // bind the i64 and return it Value::Float(f) => *f as i64, // bind the f64, round toward zero Value::Null => 0, Value::Bool(_) => 0, }}The numeric arms bind their payload (n, f) so the body can return it; the
others match the shape and return a constant. Every variant is listed, so the match is
exhaustive: add a fifth variant and this function stops compiling until you decide what it
returns, instead of quietly falling through. (You write *n because v is a &Value, so the
bindings are references; the next lesson on ownership makes that
* precise.)
Why-bridge: exhaustiveness as a refactoring tool
Section titled “Why-bridge: exhaustiveness as a refactoring tool”Why does the engine lean so hard on exhaustive match instead of a convenient _ =>
fallthrough? Because the compiler errors are the feature. When Value gains a variant
(say the team adds a Decimal cell type) the build doesn’t compile until every match
over Value has an arm for it. Each error is a precise to-do item: this coercion, that
formatter, this comparison, all flagged, none missed. The change becomes a guided chore
instead of a grep-and-pray hunt across the whole engine.
A _ => catch-all breaks that. It compiles today and absorbs tomorrow’s new variant
silently: the new shape falls into the catch-all and gets mishandled, with no error to warn
you. So in this codebase a catch-all over Value is a smell: it trades a loud compile-time
checklist for a quiet runtime bug.
Retrieval checkpoint
Section titled “Retrieval checkpoint”Connections
Section titled “Connections”You can now take a Value apart safely: read a match, bind the payloads you need, and rely
on exhaustiveness as a compiler-checked to-do list. Next: how the engine passes records
around without copying them, the ownership rules that make it
fast.
Go deeper on the Rust (optional, one-directional; the same match machinery taught from
first principles, no engine framing required):
Glossary terms used: match, arm, exhaustiveness, binding.