Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 6 of 26

Easy Primitives

Enums are real sum types

Parse a list of command lines into an Event enum and render each one back as a description.

pub fn describe_events(lines: Vec<String>) -> Vec<String>
line rendered as
quit quit
move 3 -4 move to (3, -4)
write hello world write "hello world"
color 255 0 128 color #ff0080
anything else unknown

Coordinates are i64; colour components are u8, so color 300 0 0 is not a valid line. write with nothing after it produces the empty string.

This is the most important idea in the track

An enum in C, Java or Go is a set of named integers. An enum in Rust is a sum type: a value that is exactly one of several alternatives, and each alternative may carry its own data of its own shape.

enum Event {
    Quit,                            // no data
    Move { x: i64, y: i64 },         // named fields, like a struct
    Write(String),                   // positional, like a tuple struct
    ChangeColor(u8, u8, u8),         // three positional fields
}

All four of those are one type. An Event is one of them — never two, never none. There is no valid Event outside that list. That closed-ness is what the rest of Rust’s design is built on, and it is why Option and Result are ordinary library types rather than language features.

Compare the alternative you have almost certainly written before: a struct with a kind string, four nullable fields, and a comment explaining which combinations are legal. Every function that touches it has to re-check the invariant, and the compiler helps with none of it. With a sum type the illegal combinations are not “checked” — they are unrepresentable.

The three variant shapes

Beginners learn the unit shape (Quit) and stop. All three matter:

  • Quit — a bare tag.
  • Write(String) — a tuple variant. The variant name is also a function: Event::Write has type fn(String) -> Event, so .map(Event::Write) works.
  • Move { x: i64, y: i64 } — a struct variant, when positional arguments would be ambiguous. Move(i64, i64) gives you no way to know which is x.

Note the variants of an enum live in the enum’s namespace: you write Event::Quit, not Quit. That namespacing is also why enum_variant_names will reject AppleFruit, PearFruit, PlumFruit — once the enum is called Fruit, repeating it in every variant is noise.

Reading one back out

You take a sum type apart with match, and a match on an enum must handle every variant. The starter’s render is missing an arm; you will meet E0004 and it will name the variant you forgot. Exhaustiveness gets an item of its own next — for now, notice that the compiler is doing bookkeeping you would otherwise be doing in your head.

About the parser

The starter hands you a parser. It matches on words.as_slice() with slice patterns — ["move", x, y] means “exactly three words, the first of which is move“. Slice patterns get a full item later in this track; for now read them as the shapes they look like. The parser is complete apart from one thing: it constructs a variant your enum does not have. Fix the enum.

Two harness facts worth knowing

#![allow(dead_code)] sits at the top of the file. Without it, any variant the tests never construct would be a hard error under -D warnings (variantMoveis never constructed), and you would be fighting the gate instead of learning enums.

And format!("{text:?}") on a String prints it with the quotes and with escapes applied — that is what the write rows expect.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…