Modules: Rooms for Your Code
Your snake left Phase 2 as one glorious pile of functions. Time to give every idea its own room — with a door that you design.
You will be able to…
- Group related types and functions into a
moduleand reach them with the dot:Potion.price - Describe a module's public face with a
module type(a signature) - Seal a module with
: SIGso only the signature shows - Make a type abstract so outside code can hold it but never open it
One pile of functions
Open your snake project from lesson 2.5 and scroll. Records, variants,
step, grow, contains,
draw_world, key handling, score helpers — thirty-odd
definitions standing shoulder to shoulder in one namespace. It works. It is
also getting dangerous: every function can see every other function,
move-the-snake sits one shelf away from the
move-the-menu-cursor you'll want next month, and nothing tells
you which helpers are load-bearing and which are private scaffolding.
Programmers solve this with architecture, and OCaml's unit of architecture is the module: a named room for definitions that belong together. Here's one:
Everything between struct and end is ordinary
OCaml — let bindings, type definitions, even other modules.
From outside, you reach in with the dot: Potion.price,
Potion.describe. Two rooms can now happily contain two
different move functions; the dot keeps them apart.
Watch what the toplevel answers when you define a module:
Read the answer carefully: after the : comes the module's
type — a list of what's inside and the type of each thing. That
list is called a signature, and it exists whether or not you write
it, exactly like expression types in lesson 1.1. Our next move is to take
control of it.
Signatures: designing the door
A signature is the door of the room: it lists exactly what may pass
through. You write one with module type, then seal a
module with it using a colon — the same colon you use for
(x : int), promoted to whole rooms:
heal_amount still exists — describe uses it —
but it is no longer visible from outside, because the signature doesn't
list it. Uncomment the leak line and the compiler says
Unbound value Potion.heal_amount: as far as the rest of the
program is concerned, there's a wall where that window used to be. Hiding
the scaffolding means you can rebuild it whenever you like, and nobody
outside will ever notice.
Abstract types: the crown jewel
So far we hid values. The trick that changes how you design
software is hiding a type's definition. In a signature, write
type t — full stop, no = — and the world outside
may hold a t, pass a t around, store a
t in a record… but never look inside one, never forge one,
never do anything the module didn't explicitly allow. Break it on
purpose — run this and read the error like a detective:
This expression has type Counter.t but an expression was expected
of type int. Inside the room, t is
int — incr happily computes c + 1.
Outside, Counter.t and int are different worlds.
The payoff is triple:
- Invariants hold forever. Every
tin the whole program was born inmakeand reshaped only byincr. If those keep the value sensible, no code anywhere can make a nonsense counter. - You may change your mind. Swap
type t = intfor a record with a timestamp next week; callers can't tell — they never saw theint. - The signature is the manual. The only ways to make or use a
counter are the four lines of
COUNTER. Nothing to guess.
Implement Counter so it satisfies the given
COUNTER signature: make () starts a counter
at zero, value reads it, and incr returns a
new counter, one higher — Phase 2 habits apply: no mutation, the
old counter must be left exactly as it was.
t really is an
int — nobody outside will ever know.incr c builds a new value:
c + 1. value c just hands the int back:
c.Design the door first
Professionals often run this backwards: write the signature
first, argue about it, and only then implement. The signature is
the design; the struct is mere labour. Try that workflow on a
game inventory. One deliberate spec decision: the bag is a shelf, not a
set — adding "torch" twice means you own two torches, and
size counts both.
Implement Inventory to match INVENTORY:
empty is a fresh bag, add returns a new bag
with one more item (duplicates allowed!), has tells
whether at least one such item is inside, and size counts
everything, duplicates included. Keep the representation hidden — the
tests only ever use the interface.
string list, so empty is [].add puts the item in front with
:: — duplicates land on the shelf naturally.List.mem
finds, List.length counts.Notice what the tests couldn't do: peek at your list. If you later store the bag as a sorted tree or a count-per-item table, every test — and every caller — keeps working. That's the abstraction dividend, and you'll cash it in lesson 3.2.
Sealing an open room
Last drill: you'll often meet a module that happens to match a signature but was never sealed — a room with the door propped open. Close it.
The Gold purse below works, but it's unsealed: any code
can see that t is int and mint fake money
with +. Two jobs, vault-keeper: (1) seal it with
: GOLD on the module line; (2) extend it — add
spend : int -> t -> t to both the signature
and the module (spending can never drop the purse below zero — use
max). Then prove the seal: add
let cheat = Gold.zero + 1, press Check, enjoy the type
error, and delete the line again.
module Gold : GOLD = struct … end.spend needs two lines: val spend : int -> t -> t
in the signature, and a definition in the struct.let spend n g = max 0 (g - n).Fair warning about that Check: tests can only call what the signature exposes, so they prove the purse still works — the seal itself is between you and the compiler. That's exactly why the poke matters. And here's how a sealed value looks from the toplevel:
<abstr> is the toplevel being honest: “there is a
value here, and you don't get to see inside.” Remember the look of it —
the standard library will show you the same face next lesson.
Deeper: files are modules too
In your localhost projects, every .ml file is
automatically a module — world.ml becomes
World, no module keyword needed. And the
signature? A world.mli file sitting next to it, containing
just the sig part. Lesson 3.4 turns this into a full
architecture: one room per file, one door per room, and a coach
inspecting every door.
Checkpoint quiz
Loot collected
module M = struct … endbuilds a room;M.namereaches inside.module type SIG = sig … enddescribes a door;module M : SIG = struct … endinstalls it.- Sealed modules show only what the signature lists — scaffolding stays private.
type twith no=is abstract: outsiders can hold one, never open one. Invariants become theorems; representations become swappable.- The toplevel prints
<abstr>when it, too, isn't allowed to look.
Next: the standard library takes these ideas industrial — functors, machines that build whole modules to order.