First-Class Modules: Hot-Swappable Engines

Today modules stop being furniture and become cargo: pack one into an ordinary value, hand it to a function, and swap your game's whole rendering engine while the program runs.

You will be able to…

The choice that can't wait

Everything module-shaped so far happened at compile time: you sealed Gold before the program ever ran, and MakeScoreboard (Loot) was stamped out by the compiler. But games ask questions that only have answers at runtime: draw pixels, or print ASCII into a terminal? Play sound, or stay silent because it's 11 pm? Use the debug renderer because you're replaying a bug report? An if can't choose between two modules… unless modules can be values.

Since “modules as arguments” has been on this quest's banner since day one: this is that milestone. OCaml lets you shrink-wrap any module into an ordinary value — pass it, store it, return it, pick it with an if — and unwrap it back into a full module wherever it's needed. First, the signature every engine in this lesson will satisfy. One grid cell, at a column and a row, in a colour:

Three new spells, all wearing parentheses:

Check the toplevel's reaction — a module, priced like a value:

Engines as arguments

The unpack is so common in function heads that OCaml lets you do it in the parameter list. This is the line to tattoo somewhere — a function that takes an engine:

draw_head has type (module RENDERER) -> unit: give it any engine, it draws the snake's head through it. Notice the call site: draw_head (module Ascii), no annotation needed — the parameter's type already announces the signature. This pattern has a grown-up name, dependency injection: instead of the function hard-wiring which renderer it uses, the caller injects one. Your turn — write the workhorse of this whole lesson.

Write render : (module RENDERER) -> (int * int) list -> unit. The list is a world of (col, row) cells; call the packed engine's cell once for each, in colour "lime". No peeking at which engine it is — there's no way to, and that's the design.

List.iter runs a function on every element — and your function should call R.cell.
List.iter (fun (col, row) -> R.cell col row "lime") world — the tuple pattern splits each cell on arrival.

Peek at how the Check works: the tests build a counting renderer — a module whose cell just ticks a counter — pack it, and inject it into your render. A test spy, wearing the engine uniform. The fact that tests can impersonate a renderer without touching your code is dependency injection already paying rent.

Choosing an engine at runtime

Now the punchline made literal: a function that returns one module or another depending on a plain bool — something no functor could do, because the bool doesn't exist until the game runs.

Write pick : bool -> (module RENDERER) returning the two engines given in the starter: Ascii_renderer (noisy — prints a # per cell) when the flag is true, Silent_renderer when it's false. Then play with Run: unpack your pick and call it a few times — let (module R) = pick true in R.cell 0 0 "x" — and watch the #s appear… or pointedly not.

An if/else where each branch packs a different module.
if loud then (module Ascii_renderer : RENDERER) else (module Silent_renderer : RENDERER) — both branches must pack with the same signature, or the two arms wouldn't share a type.

Before moving on, break the door on purpose. A module only fits through a package type if it truly satisfies the signature — one typo and the crate won't close:

Signature mismatch … The value cell is required but not provided — the same bouncer you met at Map.Make's gate, now checking crates at runtime's doorstep. Same contract machinery, end to end.

Two engines, one world

Grand finale. Below are two complete engines: one draws each cell as a 20-pixel square on the canvas, the other prints each cell as a line of text. Your job is a single function that renders a fixed world — a five-cell L-shape — through whichever engine it's handed. The starter then calls it twice: same world, same function, pixels and text.

Write draw_world : (module RENDERER) -> unit: render every cell of l_shape through the given engine, colour "orange", using List.iter so the cells go through in list order. Leave the two let () = draw_world … lines at the bottom untouched — they're the demo. After the Check, look up at the canvas, then down at the output pane: one world, two completely different pictures of it.

Same shape as render earlier: List.iter over l_shape, calling R.cell col row "orange".
Don't touch the two calls at the bottom — your function does the rendering, they do the injecting.

Now the moral, said plainly: draw_world consulted R.cell and asked nothing else. It has no idea whether it lit pixels, printed text, or fed a counter in a test — and it cannot know, because the signature is all it can see. The game logic never knew which engine it used. That ignorance is the architecture: next lesson, the Great Refactor injects physics rulesets and rendering backends into your snake exactly this way — swap the whole game's feel by changing which crate you hand over. One line.

Deeper: what's inside the crate?

At runtime, a packed module is roughly a record of its values — cell here — and packing/unpacking costs about as much as passing a record. The interesting part is the type-checker's seal: the package type (module RENDERER) forgets everything about the original module except the signature, which is why all engines become interchangeable the moment they're packed. (If a signature contains an abstract type t, each crate carries its own private t — OCaml keeps different crates' types apart. Our RENDERER has no types inside, which keeps today simple.)

Checkpoint quiz

Loot collected

Next: the Great Refactor — your snake gets rooms, doors, and a hot-swappable soul.