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…

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.
Write 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.

Inside the functor, 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.
For 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.
Pattern-match the pair right in the function head: let name (n, _) = n and let score (_, v) = v.
Then the whole product line is one line: 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

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.