OCaml Module 7 – Modules, signatures and functors
Functors and module constraints
Apply Map.Make and write functors of your own, and use module constraints so a functor result keeps the types its callers need to see.
What you will learn
- Apply Map.Make, Set.Make and Hashtbl.Make to a key module of your own
- Write a functor over a signature you design, and apply one to the result of another
- Choose between a sharing constraint, a destructive substitution and no constraint at all
- Distinguish an applicative functor from a generative one
Before you start
What you’ll learn · Before you start
What you will learn
- Apply Map.Make, Set.Make and Hashtbl.Make to a key module of your own
- Write a functor over a signature you design, and apply one to the result of another
- Choose between a sharing constraint, a destructive substitution and no constraint at all
- Distinguish an applicative functor from a generative one
Before you start
On this page
Map.Make (String) is one line, and the module it hands back holds 44 values, add, find, bindings, fold
and the rest, all working on string keys. Hand the same line a module that declares no compare and the
compiler stops with “The value compare is required but not provided”, because Map.Make is a function whose argument
is a module, not a special form that knows about strings.
You will apply the standard library’s three Make functors, write functors of your own over a signature you design,
and use module constraints to decide which types the result keeps visible. Every session on this page ran on
OCaml 5.5.1.
Functors are functions from modules to modules
A functor: A module that takes one or more modules as arguments and returns a new module. Map.Make is one: given a module with a type t and a compare function, it returns a module of maps whose keys have type t. More in “Maps and sets” takes a module and returns a module. Map.Make declares its parameter as
(Ord : OrderedType), a signature with exactly two items, type t and compare : t -> t -> int; nothing else about
the argument is read. Set.Make asks for the same two and Hashtbl.Make for equal and hash, which is why the
standard library ships its containers as functors: one body, any key type.
The session applies all three, with String for fares, Int for coach numbers and a module of its own that compares
seat references without regard to case; its second file hands Map.Make a module that declares only a type:
from session import run, write
# Three standard functors applied to three key modules: String for a map of fares, Int for a set of coach numbers and
# a module of our own that compares seat references without regard to case, for a hash table. The second file passes a
# module that has only the type, which Map.Make refuses.
write("fares.ml", """module SMap = Map.Make (String)
module ISet = Set.Make (Int)
module Caseless = struct
type t = string
let equal a b = String.lowercase_ascii a = String.lowercase_ascii b
let hash s = Hashtbl.hash (String.lowercase_ascii s)
end
module CTbl = Hashtbl.Make (Caseless)
let () =
let fares = SMap.(empty |> add "PUNE" 615 |> add "DELHI" 1240 |> add "KOCHI" 880) in
Printf.printf "fare to DELHI: %d\\n" (SMap.find "DELHI" fares);
SMap.iter (fun station fare -> Printf.printf " %-6s %4d\\n" station fare) fares;
let coaches = ISet.of_list [12; 4; 12; 7] in
Printf.printf "coaches: %s (%d of them)\\n"
(String.concat " " (List.map string_of_int (ISet.elements coaches)))
(ISet.cardinal coaches);
let seats = CTbl.create 8 in
CTbl.add seats "PNR4821" "S7-42";
Printf.printf "pnr4821 sits in %s\\n" (CTbl.find seats "pnr4821")
""")
write("badmap.ml", """module Station = struct
type t = string * string
end
module Legs = Map.Make (Station)
""")
run("""
ocaml fares.ml
!ocaml badmap.ml
""") Output
$ ocaml fares.ml
fare to DELHI: 1240
DELHI 1240
KOCHI 880
PUNE 615
coaches: 4 7 12 (3 of them)
pnr4821 sits in S7-42
$ ocaml badmap.ml
File "./badmap.ml", line 5, characters 14-32:
5 | module Legs = Map.Make (Station)
^^^^^^^^^^^^^^^^^^
Error: Modules do not match: sig type t = string * string end
is not included in Map.OrderedType
The value compare is required but not provided
File "map.mli", line 54, characters 4-31: Expected declaration
[exit status 2]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stdlib_functors_session.py
SMap.iter printed DELHI, KOCHI and PUNE in the order String.compare gives: the whole ordering came from that one
function, and the caseless table found pnr4821 under the stored key PNR4821 because Hashtbl.Make uses the
equal and hash it was given. The refused application names the missing item and stops with exit status 2. Step
through the application that worked:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The key module first. A functor takes a module, so one must exist before anything is applied. The standard String module has a type t and a compare that orders two strings, which is all this application needs from it.
- What the functor asks for. Map.Make declares its parameter as (Ord : OrderedType), and that signature holds exactly two items: type t and compare. The compiler checks the argument module against it, and nothing else about String is looked at.
- A module that cannot be used. A module of its own with only type t = string * string has no compare, so the check fails: "The value compare is required but not provided". The functor is never applied, and nothing of the map exists.
- The application itself. Map.Make (String) is written where a module goes. The compiler checks it and works out the result types; when the program runs, the body runs with String as its argument and builds 44 values from the one compare.
- The module that comes out. SMap is an ordinary module. Its key is String.t, because the functor result says key = Ord.t and the argument fixes what Ord.t is; the map type 'a t is kept abstract, so only the module's own operations build one.
- Using the result. SMap.add and SMap.find now work on string keys with no further mention of String, and the order the map keeps is the one compare gave: find for DELHI answered 1240.
Writing a functor of your own, and applying one to another
A functor of your own is written like a function, with the parameter and its signature in brackets:
module ListShow (E : SHOW) = struct … end. Inside the body only what SHOW declares may be used, which is why the
body compiles once and then works for every module that matches: there is no way for it to depend on a detail of one
argument. The session defines SHOW, a module of integers, and two functors over it:
from session import toplevel
# A functor of our own: SHOW is the argument signature, ListShow and OptionShow are functors from a SHOW module to a
# SHOW module, and the last phrases apply them, one of them to the result of the other.
toplevel("""
module type SHOW = sig
type t
val show : t -> string
end;;
module IntShow = struct
type t = int
let show = string_of_int
end;;
module ListShow (E : SHOW) : SHOW with type t = E.t list = struct
type t = E.t list
let show xs = "[" ^ String.concat "; " (List.map E.show xs) ^ "]"
end;;
module OptionShow (E : SHOW) : SHOW with type t = E.t option = struct
type t = E.t option
let show = function None -> "-" | Some x -> E.show x
end;;
module Fares = ListShow (IntShow);;
Fares.show [615; 1240; 880];;
module Waitlist = ListShow (OptionShow (IntShow));;
Waitlist.show [Some 12; None; Some 7];;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# module type SHOW = sig
type t
val show : t -> string
end;;
module type SHOW = sig type t val show : t -> string end
# module IntShow = struct
type t = int
let show = string_of_int
end;;
module IntShow : sig type t = int val show : int -> string end
# module ListShow (E : SHOW) : SHOW with type t = E.t list = struct
type t = E.t list
let show xs = "[" ^ String.concat "; " (List.map E.show xs) ^ "]"
end;;
module ListShow :
(E : SHOW) -> sig type t = E.t list val show : t -> string end
# module OptionShow (E : SHOW) : SHOW with type t = E.t option = struct
type t = E.t option
let show = function None -> "-" | Some x -> E.show x
end;;
module OptionShow :
(E : SHOW) -> sig type t = E.t option val show : t -> string end
# module Fares = ListShow (IntShow);;
module Fares : sig type t = IntShow.t list val show : t -> string end
# Fares.show [615; 1240; 880];;
- : string = "[615; 1240; 880]"
# module Waitlist = ListShow (OptionShow (IntShow));;
module Waitlist :
sig type t = OptionShow(IntShow).t list val show : t -> string end
# Waitlist.show [Some 12; None; Some 7];;
- : string = "[12; -; 7]"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 show_functor_session.py
The toplevel printed the type of the functor as (E : SHOW) -> sig type t = E.t list val show : t -> string end: an
arrow from a module to a signature. Because a functor returns a module, its result can be another functor’s argument,
and ListShow (OptionShow (IntShow)) printed "[12; -; 7]", with - where the list held None. Each application
is a separate module with its own show, so nothing is shared between the two.
Checkpoint
Check: Applying a functor to a module · Writing a functor over your own signature
A module constraint decides which types the result shows
Module constraints are written after a signature: SHOW with type t = int is the SHOW signature with its abstract
type pinned to int, which the session prints as sig type t = int val show : t -> string end. In a functor’s
result, that sharing constraint: A constraint written with = after a signature, as in SHOW with type t = int, which keeps the type declaration and adds an equation to it, so callers may use the two names interchangeably. is what keeps the caller’s type visible. Ascribe
the bare signature instead and t becomes abstract, as this session records:
from session import toplevel
# The same functor body three ways: ascribed to a bare SHOW (t abstract, so a list is refused), ascribed with a
# sharing constraint (that is show_functor_session.py) and with no signature at all, which hides nothing. The last
# phrase puts a sharing constraint on a signature on its own.
toplevel("""
module type SHOW = sig
type t
val show : t -> string
end;;
module IntShow = struct
type t = int
let show = string_of_int
end;;
module Opaque (E : SHOW) : SHOW = struct
type t = E.t list
let show xs = "[" ^ String.concat "; " (List.map E.show xs) ^ "]"
end;;
module Hidden = Opaque (IntShow);;
Hidden.show [615; 880];;
module Plain (E : SHOW) = struct
type t = E.t list
let show xs = "[" ^ String.concat "; " (List.map E.show xs) ^ "]"
let count = List.length
end;;
module Open = Plain (IntShow);;
Open.show [615; 880];;
module type SHOW_INT = SHOW with type t = int;;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# module type SHOW = sig
type t
val show : t -> string
end;;
module type SHOW = sig type t val show : t -> string end
# module IntShow = struct
type t = int
let show = string_of_int
end;;
module IntShow : sig type t = int val show : int -> string end
# module Opaque (E : SHOW) : SHOW = struct
type t = E.t list
let show xs = "[" ^ String.concat "; " (List.map E.show xs) ^ "]"
end;;
module Opaque : (E : SHOW) -> SHOW
# module Hidden = Opaque (IntShow);;
module Hidden : sig type t = Opaque(IntShow).t val show : t -> string end
# Hidden.show [615; 880];;
Line 1, characters 12-22:
1 | Hidden.show [615; 880];;
^^^^^^^^^^
Error: This constructor has type 'a list
but an expression was expected of type Hidden.t = Opaque(IntShow).t
# module Plain (E : SHOW) = struct
type t = E.t list
let show xs = "[" ^ String.concat "; " (List.map E.show xs) ^ "]"
let count = List.length
end;;
module Plain :
(E : SHOW) ->
sig
type t = E.t list
val show : E.t list -> string
val count : 'a list -> int
end
# module Open = Plain (IntShow);;
module Open :
sig
type t = IntShow.t list
val show : IntShow.t list -> string
val count : 'a list -> int
end
# Open.show [615; 880];;
- : string = "[615; 880]"
# module type SHOW_INT = SHOW with type t = int;;
module type SHOW_INT = sig type t = int val show : t -> string end
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sharing_session.py
Hidden.show [615; 880] was refused with “This constructor has type ’a list but an expression was expected of type
Hidden.t = Opaque(IntShow).t”: the result does hold lists, but its signature does not say so. Three ascriptions, three
outcomes:
| The functor’s result is written | What callers see | Use it when |
|---|---|---|
no signature at all (Plain) |
every type and every value, helpers included | the functor is internal to one file |
: SHOW (Opaque) |
show only, with t abstract |
the representation must stay private |
: SHOW with type t = E.t list |
show, and t is known to be a list |
callers pass and read the values |
Two more constraints, and two kinds of functor, follow. with type t := int removes t from the
signature instead of defining it, which lets two signatures that both declare t be included side by side. The same
:= also removes a submodule, or names a type for the rest of one signature.
module type of M reads a signature off a module that already exists, so a wrapper need not repeat its declarations.
A functor with a module parameter is applicative: two applications to the same argument give the same types. And a
functor whose parameter is () is generative: each application makes abstract types of its own, which two
applications can never mix.
Key idea
A functor body may use only what its parameter’s signature declares, and the result’s signature decides what callers may use: a sharing constraint is how a type the argument supplied stays visible after the application.
Checkpoint
Check: What the result signature shows
Removing a type from a signature, and reusing a module's own signature Pro: opens with a Pro, Premium or Ultimate pass
Two signatures that each declare type t cannot both be included: the compiler refuses the shadowing. Destructive
substitution and module type of are the two constraints that solve it, and each has an edge that surprises people
the first time. Both sessions below ran on OCaml 5.5.1.
In this part:
- Why two includes clash, and what destructive substitution takes away
- A signature read off a module, and the strengthening it leaves out
- Substituting a module, and naming a functor result inside a signature
3 runnable examples (Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Applicative and generative functors, with fresh types per application Pro: opens with a Pro, Premium or Ultimate pass
Apply Map.Make (String) twice and the two results have the same types, so a map built by one is accepted by the
other. Apply a functor whose parameter is () twice and the two results share nothing at all. Which of the two you
write is a design decision, and the sessions below show what each choice allows.
In this part:
- Two applications that agree, and two that cannot
- Choosing between the two kinds
- One functor, two ordered types: intervals of seats and of days
1 diagram · 2 runnable examples (Python) · about 8 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: functors and the constraints on their results
The questions cover the three sessions above: what a functor asks for, what composing two gives, and which types the result shows.
Practice that adapts to you
Practise this lesson
9 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): what does a functor take, and what does it hand back?
A functor takes a module and returns a module: its parameter has a signature instead of a type, and its body may use
only what that signature declares. Map.Make declares its parameter (Ord : OrderedType), a signature with type t
and compare : t -> t -> int, and Map.Make (String) returns a
module whose key is String.t and whose operations keep the order String.compare defines. The compiler checks
the argument against the signature and works out the result’s types, and the body runs once per application when the
program runs. That is how one body serves every key type, as long as the argument supplies compare.
-
Mid to senior
A library of yours exports Make (E : ELEMENT) : S. Callers report that they can hold values of the result type but can neither build nor read them, and the internal representation must stay private. What do you change in the functor, and what do you refuse to change?
-
Senior to staff
You write a functor that builds a counter module with a mutable table inside it. Two parts of one binary apply it to the same argument module and must not share the table. Which kind of functor do you write, and what goes wrong with the other choice?
-
Mid to senior
When would you build a data structure as a functor over an element module instead of taking a comparison function as an argument of each operation, as List.sort does? Give the cost of each choice.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What is a functor?
A function from modules to modules. Its body may use only what its parameter's signature declares, and the compiler checks an application such as Map.Make (String) against that signature.
-
When does the body of a functor run?
Once per application, when the program runs. The compiler only checks the argument against the parameter's signature and works out the result's types.
-
What do Map.Make, Set.Make and Hashtbl.Make ask of their argument module?
Map.Make and Set.Make need type t and compare; Hashtbl.Make needs equal and hash. An argument missing one is refused with the name of the missing item.
-
What does the one line Map.Make (String) give back?
A module of 44 values, such as add, find, bindings and fold, all working on string keys.
-
How can one functor's result be another functor's argument?
A functor returns a module, so applications nest: ListShow (OptionShow (IntShow)) printed a list of optional numbers.
-
What does a sharing constraint such as with type t = E.t list do to a functor's result?
It keeps a type the argument supplied visible to callers; with a bare result signature the type would be abstract.
Key takeaways
- A functor is a function from modules to modules: its body may use only what its parameter’s signature declares, and
the compiler checks an application such as
Map.Make (String)against that signature. Map.MakeandSet.Makeask an argument module fortype tandcompare,Hashtbl.Makeforequalandhash, and an argument missing one of them is refused with the name of the item.- A functor returns a module, so one application can be another’s argument, which is how
ListShow (OptionShow (IntShow))printed a list of optional numbers. - The result’s signature decides what callers see: a bare signature makes the type abstract, and a sharing constraint
such as
with type t = E.t listkeeps the caller’s own type visible.
Terms in this lesson
- sharing constraint
- A constraint written with = after a signature, as in SHOW with type t = int, which keeps the type declaration and adds an equation to it, so callers may use the two names interchangeably.
References
- The OCaml Manual: A simple example of modules and functors (Inria and the OCaml developers)
- The OCaml Manual: Substituting inside a signature (Inria and the OCaml developers)
- The OCaml Manual: Generative functors (Inria and the OCaml developers)
- The OCaml Manual: Recovering the type of a module (Inria and the OCaml developers)
- The OCaml Manual: Module types (Inria and the OCaml developers)
- The OCaml Manual: Map.Make (Inria and the OCaml developers)
- The OCaml Manual: Hashtbl.Make (Inria and the OCaml developers)
- Functors (OCaml.org)
Report a problem with this lesson
Kept only in this browser. Your Learn progress