Enums — modeling one-of-several
The Rust question for this lesson: how do you model something that is exactly one of a few kinds — where the kinds are mutually exclusive, and each kind may carry its own data?
That pattern is everywhere. A traffic light is red, amber, or green. A keypress is a
letter, a digit, or an arrow — and the letter case carries which letter. The kinds
never overlap (never red and green) and never vanish (it’s always some colour).
Rust has a purpose-built tool for this, the enum, and learning it well is the
single biggest step from “I can read Rust syntax” to “I understand how Rust models a
problem.” We’ll learn it on a tiny example with no engine in sight, then watch the
exact same machinery turn up at the core of a real data engine.
A small model first
Section titled “A small model first”Forget data engines for a minute — we’re drawing shapes. A shape is one of a few kinds, and each kind needs different information: a circle needs a radius, a rectangle needs a width and a height, a bare point needs nothing at all.
#[derive(Debug)]enum Shape { Point, // a kind that carries no data Circle(f64), // carries one number: the radius Rectangle(f64, f64), // carries two: width, then height}Each line inside the enum is a variant — one kind a Shape is allowed to be.
Two flavours appear here:
Pointis a unit variant: just a name, no payload.Circle(f64)andRectangle(f64, f64)are tuple variants: a name plus data tucked inside the parentheses. Thef64is the type of that data — a 64-bit floating-point number. Rust makes you state the type; that precision is deliberate.
The promise the compiler now enforces: a Shape value is exactly one of these at
any moment. Never a circle that’s also a rectangle. Never a shape that is somehow
none of them.
You build one by naming the variant and supplying its data:
let a = Shape::Circle(2.0);let b = Shape::Rectangle(3.0, 4.0);let c = Shape::Point; // unit variant: nothing to supplyPress ▶ run — then change a number, add another Shape::Point, or give Circle
two arguments and read the compiler’s complaint.
> output appears here — press Run
// quick check
In Rectangle(f64, f64), what do the two f64s represent?
A tuple variant carries one or more typed payloads. Rectangle(f64, f64) is a single kind of Shape that holds two numbers inside it.
Why this is worth the fuss
Section titled “Why this is worth the fuss”In a language without enums, you fake “one of several kinds” — a status integer plus
a comment about what each number means; a bag of nullable fields where only some are
set at a time; a class hierarchy you hope nobody subclasses wrongly. Every one of
those lets an illegal state exist: status 7 that means nothing, a “circle” with a
width but no radius. Rust’s enum makes “exactly one kind, with exactly its own data”
a fact the type system guarantees — illegal combinations simply cannot be
constructed.
That guarantee is the foundation the next two lessons build on: because a value is
provably one known kind, the compiler can force you to handle every kind (match,
lesson 2) and can model “maybe absent” as just another enum (Option, lesson 3).
The same machinery, running an engine
Section titled “The same machinery, running an engine”Here is where it earns its keep. Clinker is a data engine: it pulls in rows of
data and runs transformations over them. The most central type in the whole engine —
the single “cell of data” everything else is assembled from — is an enum, and it is
exactly the pattern you just wrote, scaled up:
clinker-record ·value.rs ·Value type @47d2e12
#[derive(Debug, Clone)]pub enum Value { Null, // unit variant — like Shape::Point Bool(bool), // tuple variants — like Shape::Circle Integer(i64), Float(f64), String(FieldStr), Date(NaiveDate), DateTime(NaiveDateTime), Array(Vec<Value>), // ← holds more Values Map(Box<IndexMap<Box<str>, Value>>), // ← holds more Values}Read it with Shape in mind and it’s familiar: Null is a unit variant (a cell that’s
empty), and Integer(i64) / Bool(bool) are tuple variants carrying typed data — the
same two flavours, just nine kinds instead of three. (Ignore FieldStr, Box, and
IndexMap for now; they’re optimisations we unpack in later lessons.)
Two things Value does that our Shape toy didn’t:
- It nests.
Array(Vec<Value>)is a list ofValues, andMapholdsValues too. An enum whose variants contain the enum itself is recursive — and that’s how nine flat kinds describe data of any depth: a list of numbers, a record of fields, a list of records of lists. - The set is deliberately closed — and that’s a feature. Clinker has a little
expression language, CXL, that runs a formula over every row (think of a
spreadsheet formula applied down a column of a million rows). For CXL to be checked
before a job runs — so a typo fails at planning time, not halfway through the file
— the kinds of value must be fixed and known in advance. This
enumis that fixed set, written down once and trusted by the language, the on-disk format, the planner, and the runtime alike.
The #[derive(Debug, Clone)] on top is the very shortcut you used on Shape: it
auto-generates the code to print the value (Debug) and to deep-copy it (Clone).
Your turn
Section titled “Your turn”A read-only design exercise — nothing in the repo is edited.
A calendar app must store one reminder trigger, which is exactly one of: at a specific timestamp, N minutes before the event, or never. Sketch the
enum— variant names and the payload each needs. Then answer: if you were storing the “N minutes” value inside clinker, whichValuevariant fits, and why wouldValue::Stringbe the wrong call?
💡 Hint 1
Three kinds → three variants. “Never” carries nothing (a unit variant); the other two each carry one piece of data. For the clinker half: “minutes” is a whole number you’ll later do arithmetic on.
Show solution
enum ReminderTrigger { Never, // unit variant At(DateTime), // a timestamp MinutesBefore(u32), // a count of minutes}Inside clinker, the minutes belong in Value::Integer(i64) — it’s a whole number you’ll
compare and subtract. Value::String would force every later step to re-parse the text
back into a number (slower, and able to fail at runtime on bad input), throwing away the
type guarantee the enum exists to give you.
Common misconceptions
Section titled “Common misconceptions”- “An enum is just named integers, like in C.” No. A Rust enum is a sum type:
each variant can carry its own typed payload (
Circle(f64),Integer(i64),Array(Vec<Value>)). The C kind is only the special case where no variant carries data. - “A value might be two variants at once, or none.” Never. It is exactly one variant at any instant — that is the guarantee the compiler gives you for free.
The verify commands run in the clinker checkout (read-only, pinned 47d2e12).
test_value_enum_size is clinker’s own test pinning the in-memory size of Value — a
first hint of the memory reasoning we reach in the lifetimes-and-memory phase.
Where this leads
Section titled “Where this leads”You can now define a type that is one of several kinds. The obvious next question is:
given such a value, how do you do something different for each kind — and have the
compiler stop you if you forget one? That tool is match, and it’s lesson 2 —
where we’ll put Shape (and Value) to work.