Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 15 minutes
  • Examples run with ocaml 5.5.1, opam 2.6.0, dune 3.24.2 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: Functors are functions from modules to modules · Writing a functor of your own, and applying one to another · A module constraint decides which types the result shows. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

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:

Three standard functors applied, and one application refused Python · stdlib_functors_session.py
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:

Interactive · stage by stage One application of Map.Make, stage by stage Step through the application. Each stage is one thing the compiler has in hand, and its sub-label says what that stage fixes.
One application of Map.Make, stage by stage, step 6 of 6. Showing module String, Map.OrderedType, Map.Make (String), module SMap, SMap.find "DELHI". In focus: SMap.find "DELHI".no compare: refusedmodule Stringtype t = string, compareMap.OrderedTypewhat Map.Make asks forMap.Make (String)the applicationmodule SMapkey = String.tSMap.find "DELHI"fare 1240

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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:

A SHOW signature, two functors over it, and one applied to the other Python · show_functor_session.py
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

  1. Question 1 of 2 Which items must a module supply to be the argument of Map.Make?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • type t
    • compare : t -> t -> int

    The parameter is declared (Ord : OrderedType), and that signature holds exactly type t and compare. The session handed Map.Make a module with only a type and got "The value compare is required but not provided".

  2. Question 2 of 2 Waitlist is ListShow (OptionShow (IntShow)). What did Waitlist.show [Some 12; None; Some 7] print?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: "[12; -; 7]"

    The inner application fixes the element type, so the outer one shows a list of optional integers. Composition is ordinary application, one module at a time.

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:

The same functor body with no constraint, a bare signature and a sharing constraint Python · sharing_session.py
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

  1. Question 1 of 2 Opaque (E : SHOW) : SHOW has a body with type t = E.t list. Why was Hidden.show [615; 880] refused?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The ascribed signature leaves t abstract, so the result type is Opaque(IntShow).t, not a list

    Ascribing a signature hides everything the signature does not state. A sharing constraint, SHOW with type t = E.t list, is what puts the equation back.

  2. Question 2 of 2 You write a functor that builds a queue from an element module. Callers must push and pop elements, but must never see how the queue is stored. What goes on the result?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A signature with an abstract queue type and a sharing constraint for the element type

    The two decisions are separate: hide the structure, show the element type. That is the usual shape of a library functor.

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.

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.

  1. Question 1 of 9 What is a functor in OCaml?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: A function from modules to modules, whose parameter is declared with a signature

    The toplevel prints a functor type as an arrow from a parameter module to a signature, as it did for (E : SHOW) -> sig type t = E.t list val show : t -> string end.

  2. Question 2 of 9 You want a hash table whose keys are seat references compared without regard to case. What must the argument module of Hashtbl.Make provide?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: type t, an equal that ignores case, and a hash that agrees with it

    equal and hash must agree: two keys that are equal must hash the same, or a lookup can miss a key that is present.

  3. Question 3 of 9 A colleague writes module Legs = Map.Make (Station) where Station declares only type t = string * string. What happens, and what is the smallest fix?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: It fails with "The value compare is required but not provided"; add a compare to Station

    Stdlib.compare on a pair of strings is one acceptable compare, and a hand-written one that orders the legs by origin and then destination is another.

  4. Question 4 of 9 Inside module ListShow (E : SHOW), what may the body use from E?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: Only E.t and E.show, the items SHOW declares

    A functor parameter is an interface, not a module you can inspect. Everything else the argument holds is invisible inside the body.

  5. Question 5 of 9 The toplevel printed ListShow as (E : SHOW) -> sig type t = E.t list val show : t -> string end. What does the E in E.t list refer to?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: The module given at each application, so the result type follows the argument

    A functor type is dependent in that sense: the result signature may mention the parameter, and the application substitutes the argument for it.

  6. Question 6 of 9 You have ListShow and OptionShow over SHOW. Which application shows a value such as [Some 12; None], and why that order?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: ListShow (OptionShow (IntShow)), because the outer functor decides the outer structure

    Read the application from the inside out: the innermost module is the element type, and each functor wraps the structure around it.

  7. Question 7 of 9 What does a sharing constraint such as SHOW with type t = int do to a signature?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: It keeps the declaration and adds the equation, so callers may use t and int interchangeably

    The constraint is what lets a functor result stay usable: without the equation, callers hold values of a type they cannot build or read.

  8. Question 8 of 9 This functor compiles, but callers cannot pass a list to the result. Which change makes the result accept lists, without publishing the helper functions of the body?

    Read the code, then choose one answer.

    module Opaque (E : SHOW) : SHOW = struct
      type t = E.t list
      let show xs = "[" ^ String.concat "; " (List.map E.show xs) ^ "]"
    end
    Show the answer: Question 8 of 9

    Answer: Ascribe SHOW with type t = E.t list

    Hiding and showing are separate decisions, and the constraint is how you make one of them without the other.

  9. Question 9 of 9 A functor in a single file is used only by the functions below it, and you want to change its representation freely while writing it. What is reasonable for its result?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: No signature while the file is private to one library, with a signature added when the module is exported

    The session showed all three outcomes on one body: no signature, a bare signature and a constraint. The decision belongs to the moment a module becomes something others use.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

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

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

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

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

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

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

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

  6. 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.Make and Set.Make ask an argument module for type t and compare, Hashtbl.Make for equal and hash, 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 list keeps 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.