Functors: Module Factories
You've built modules by hand. Now meet the machines that build them for you: functions from modules to modules.
You will be able to…
- Instantiate a standard-library functor:
Map.Make (String) - Use the resulting map with
add,find_optandfold - Explain why a functor's argument needs a signature
- Write your own functor and stamp out several modules with it
The warehouse problem
Your Inventory from last lesson keeps items in a list, so
has strolls down the shelf item by item. Fine for a hero's
backpack; grim for a trading-post warehouse that answers thousands of
“got any torches?” queries per tick. The standard library ships the right
tool: Map, a fast, sorted, immutable dictionary.
But you can't use Map directly — try Map.add
someday and watch it sulk. A map keeps keys sorted, so it must
know how to compare them. Compare how? Strings alphabetically, grid
positions by x then y, players by… see, it depends. So the library refuses
to guess and ships Map as a factory: hand me a
module that says what the keys are and how they compare, and I'll build
you a complete dictionary module for those keys. A module-eating,
module-producing machine like that is called a functor. Applying
one looks like a function call wearing its Sunday best:
String — the everyday stdlib module — moonlights here as
the factory's raw material: it happens to carry a type t = string
and a compare, which is all Map.Make asks for.
Out comes SMap, a made-to-measure module with some forty
functions: add, find_opt, fold,
mem, remove… A toplevel session shows the fruits
— and a familiar face:
<abstr>! The pros at the standard library sealed
their map type, exactly like you sealed Gold — you may hold
an SMap.t, never rummage inside one. And
find_opt returns the option you befriended in
lesson 2.2, because “not in stock” is an answer, not an accident.
Run the trading post on SMap. A stock is an
int SMap.t mapping item names to quantities. Write:
count : string -> int SMap.t -> int — how many of
that item the stock holds; 0 if it's absent.
restock : string -> int -> int SMap.t -> int SMap.t —
a new stock with qty more of the item (a
delivery adds to the pile, it doesn't replace it — and it must work
for items never seen before).
SMap.find_opt name stock answers
Some n or None — match on it.count first; then
restock is one line that leans on it:
SMap.add name (count name stock + qty) stock.Why the argument needs a signature
What exactly does Map.Make demand of its argument? Its
parameter is typed with a signature, Map.OrderedType:
sig type t val compare : t -> t -> int end — a shopping
list. Show up without an item on the list and the factory turns you away
at the gate. Break it on purpose:
The error says it straight: The value compare is required but not
provided. And that's the answer to why functor parameters carry
signatures: the factory's body is compiled once, knowing nothing
about the argument except that shopping list — so the list must name
everything the body relies on, and every applicant must bring all of it.
Same contract discipline as your sealed modules, pointing the other
way.
Write your own factory
Time to run a factory of your own. Every game you'll ever ship has scoreboards: players ranked by points, loot ranked by value, speedruns ranked by time. The algorithm — find the best, sum the total — never changes; only the type of thing being scored does. That's the functor-shaped hole. The syntax, piece by piece:
Inside the body, S.name and S.score are
all you know about S — the signature is a blindfold,
and the blindfold is what makes the factory reusable. Note that
Monster never declared itself SCORABLE: like
String at the map factory's gate, it fits because it has the
right pieces with the right types. OCaml checks the shape, not the
pedigree.
Write MakeScoreboard, a functor that takes any module
S : SCORABLE and produces a module with:
total : S.t list -> int — the sum of all scores
(0 for an empty list).
best : S.t list -> string — the name of the
entry with the highest score, or "nobody" for the empty
list. Tests never feed you tied scores, so any fold that keeps the
strictly-highest entry wins.
S.name and
S.score are the only things you know about S
— and they're all you need.total is a Phase-2 fold:
List.fold_left (fun acc e -> acc + S.score e) 0 entries.best, match: [] -> "nobody";
otherwise fold over the tail keeping whichever entry scores higher,
then S.name the champion.The payoff: one factory, many boards
Here's why you bothered. The factory is written and tested once; now every scoreable thing in your game gets a scoreboard for the price of three lines. Yesterday players — today, the loot pile.
The factory ships pre-built in the starter. Feed it a new material:
define Loot, where t = string * int — an
item name and its gold value, so name returns the first
half and score the second — then stamp out
module LootBoard = MakeScoreboard (Loot).
Loot needs exactly three things:
type t = string * int, a name, a
score. No sealing required — the functor's door does the
checking.let name (n, _) = n and let score (_, v) = v.module LootBoard = MakeScoreboard (Loot).Count the cost of the second scoreboard: five lines, zero new logic,
zero new tests for the algorithm. When the algorithm grows a
median or a top3, every board — players, loot,
quests — gets it simultaneously. That's what “module factory” buys.
Deeper: functors are checked, not pasted
A functor isn't a copy-paste template like C++ macros: its body is
type-checked once, against the parameter signature alone. That's why the
blindfold matters — if the body only assumed things listed in
SCORABLE, then every lawful application is correct
by construction. No “works for Player, mysteriously explodes for Loot”.
The application MakeScoreboard (Loot) happens entirely at
compile time; by the time your game runs, LootBoard is as
ordinary as a hand-written module. Hold that thought for next lesson,
where we make the choice later than compile time.
Checkpoint quiz
Loot collected
Map.Make (String)— apply a stdlib factory, get a whole dictionary module:add,find_opt,fold, forty friends.- Maps are immutable and their type is abstract —
<abstr>is the pros doing lesson 3.1 at industrial scale. - A functor's parameter signature is a shopping list: the body relies on exactly those items, applicants must bring them all.
- Your own functors turn “same algorithm, different type” into three-line product lines.
Functors choose their module at compile time. But some choices
can't wait — pixels or ASCII? walls that kill or walls that wrap? Next
lesson, modules become ordinary values you can pick with an
if.