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…

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!

Left/Right only touch dx; Up/Down only touch dy.
Up must make y smaller: | 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.

A match with four arms, each returning another constructor.
Face Up, turn clockwise a quarter: you now face Right. So | 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.

Replace each _ with a name to grab the payload: | Coin n -> ….
"Never below zero" is 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".

Match the exact strings: "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

Next: the snake grows a body — lists, and the recursion that walks them.