Variants & Pattern Matching
Numbers can't say "the snake is heading Left" and strings
can't promise "there is no save file, and that's fine". Variants let you
invent exactly the values your game needs — and match makes
forgetting a case impossible.
You will be able to…
- Define variant types like
dirand take them apart withmatch - Attach data to constructors (
Coin of int) and extract it by pattern - Let exhaustiveness checking find the cases you forgot
- Use
optioninstead of null — and handleSome/Nonehonestly
Four directions, no impostors
How would you store "which way is the snake heading"? An int where 0 means up and 3 means left? That's the ancient bug farm: nothing stops a 7 from sneaking in. OCaml lets you declare a type whose values are exactly the ones you list — nothing more can ever exist:
Up, Down, Left,
Right are called constructors — they always start
with a capital letter, and they are real values, as first-class as
42. To ask "which one have I got?", use
match, the fork in the road:
Read it as: "look at d; take the first arm whose pattern
fits; the arrow's right side is the answer." All arms must produce the
same type of answer — here, strings. Now let's make directions
do something. On our canvas, remember from lesson 1.4:
y grows downward. The top-left corner is (0, 0), so moving
Up means subtracting from y.
Write delta : dir -> int * int returning the
(dx, dy) of one step in that direction —
Right is (1, 0), and mind the y axis!
| Up -> (0, -1). Down is its mirror.Break it on purpose: the forgotten case
Here's the superpower. Delete-a-case is a bug in every language;
in OCaml it's a conversation. Run this — it's the
arrow function with Right deliberately
missing — and read the warning carefully:
The engine points at the match and says: this pattern-matching is
not exhaustive… Here is an example of a case that is not matched:
Right. It names the exact constructor you forgot. This is why
variants beat int-codes: months from now, when you add a fifth direction
(a portal? a pause?), the compiler will list every match in the
game that needs a new arm. Refactoring stops being archaeology and
becomes a checklist. (Ignore the warning and call arrow
Right, and you'd get a Match_failure crash at
runtime — warnings are love letters; read them.)
Write turn_right : dir -> dir — a quarter turn
clockwise: Up becomes Right,
Right becomes Down, and so on around.
Handle all four cases; no wildcards needed.
| Up -> Right.Constructors that carry loot
Constructors can also carry data. A game event isn't just a label — picking up a coin comes with a value, a hit comes with damage:
Coin of int declares a constructor that's a little box
with an int inside. To open the box, match with a pattern that names the
contents: | Coin n -> … — inside that arm,
n is the coin's value. One function can now settle
what any event does to your score:
Write apply : event -> int -> int where the
second argument is the current score. Coin n adds
n points; Hit n costs n
points but the score never drops below 0 (last lesson's
max!); Heartbeat awards 1 point for simply
staying alive.
_ with a name to grab the payload: | Coin n -> ….max 0 (score - n), exactly like hit in lesson 2.1.option: the honest maybe
Last trick, and it kills a famous villain. Most languages let any
value secretly be null, and the crash happens far from the
lie. Tony Hoare, who invented null references, calls them his
"billion-dollar mistake". OCaml has no null. When a value might be
absent, you say so in the type: int option is
either Some 42 or None — a plain variant from
the standard library. And because it's a variant, the compiler makes you
match both cases before you can touch the contents:
Try bonus_of_level 3 too. There is no way — none — to
use the prize without deciding what happens when it isn't there. The
"forgot to check for null" bug is not fixed here; it is
unrepresentable. You'll meet option constantly:
next lesson, functions like List.find_opt answer searches
with exactly this type.
The high-score table may be empty. Write
describe : int option -> string:
for Some n return "score: " followed by
the number (use string_of_int); for None
return exactly "no score yet".
"score: " has one space; "no score yet" has no full stop.| Some n -> "score: " ^ string_of_int n — n is the number inside the box.Checkpoint quiz
Loot collected
- A variant type lists every value that can exist:
type dir = Up | Down | Left | Right. Constructors are Capitalized values. matchtakes one arm per shape; arms bind payloads by name:| Coin n -> ….- The exhaustiveness warning names the case you forgot — a refactoring superpower, lost the moment you hide behind
_. optionreplaces null:Some vorNone, and the compiler insists you handle both.- On this canvas,
Upis(0, -1): y grows down.
Next: the snake grows a body — lists, and the recursion that walks them.