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

Modules, files and namespaces

How each OCaml .ml file becomes a module, how dune wraps a library's modules, and when to use open, local opens, include and module aliases.

  • 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 lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain how each .ml file becomes a module and how dune names the modules of a library
  • Define nested modules and reach their items through paths and module aliases
  • Choose between open, let open, M.( ... ) and include for a given piece of code
  • Avoid name clashes with qualified names, open! and warnings 44 and 45

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how each .ml file becomes a module and how dune names the modules of a library
  • Define nested modules and reach their items through paths and module aliases
  • Choose between open, let open, M.( ... ) and include for a given piece of code
  • Avoid name clashes with qualified names, open! and warnings 44 and 45

Before you start

On this page

Two files, vector.ml and shape.ml, are already two modules, Vector and Shape, and compiled in the wrong order the second stops at its third line with Error: Unbound module Vector. Put the same files in a dune library called geometry and they move into its namespace, so a program outside it must write Geometry.Vector.make; and in a third program of this lesson one careless open made close 1.0 1.005 answer false without a single warning.

You will explain how a file becomes a module and what dune names it, reach modules nested inside a file through paths and aliases, choose between open, let open, M.( ... ) and include, and keep an open from hiding a name you meant, with qualified names, open! and warnings 44 and 45.

Every .ml file is already a module

The geometry library of this lesson has two files. vector.ml defines a record type t for a point on a plane and the functions make, add, scale, length and to_string; shape.ml defines circles and squares and uses Vector.t for their centres and corners. Neither file says module Vector = ... anywhere:

The two files of the geometry library

lib/vector.ml · geometry/lib/vector.ml

(* vector.ml: points and moves on a plane. The file is the module Vector. *)
type t = { x : float; y : float }

let make x y = { x; y }
let add a b = { x = a.x +. b.x; y = a.y +. b.y }
let scale k v = { x = k *. v.x; y = k *. v.y }
let length v = Float.sqrt ((v.x *. v.x) +. (v.y *. v.y))
let to_string v = Printf.sprintf "(%g, %g)" v.x v.y

lib/shape.ml · geometry/lib/shape.ml

(* shape.ml: shapes on the plane. Inside the library, Vector needs no prefix. *)
type t =
  | Circle of Vector.t * float (* centre and radius *)
  | Square of Vector.t * float (* lower-left corner and side *)

let area = function
  | Circle (_, r) -> Float.pi *. r *. r
  | Square (_, side) -> side *. side

let centre = function
  | Circle (c, _) -> c
  | Square (corner, side) -> Vector.add corner (Vector.make (side /. 2.) (side /. 2.))

Each file is a module: A named collection of definitions (values, types, exceptions and other modules), reached from outside with a dot, as in List.length. Every .ml file is a module named after the file. just by existing, and the compiler names it after the file. Compile the two by hand with ocamlc, shape.ml first, then load both into the toplevel:

Two files compiled by hand, in both orders Python · files_session.py
from session import files, run, toplevel, write

# vector.ml and shape.ml, the two files of the geometry library (examples/geometry/lib/), compiled by hand. Each file
# is a module named after it, and shape.ml uses Vector, so it compiles only once vector.ml has been compiled. ocamlc -i
# prints the interface the compiler inferred for Shape. A file name that is not a module name gives warning 24. The
# toplevel then loads both compiled modules (the .cmo files on its command line) and calls them by name.
files("geometry/lib/vector.ml=vector.ml", "geometry/lib/shape.ml=shape.ml")
run("""
!ocamlc -c shape.ml
ocamlc -c vector.ml
ocamlc -c shape.ml
ls
ocamlc -i shape.ml
""")
write("my-shapes.ml", "let n = 1\n")
run("""
ocamlc -c my-shapes.ml
ls *.cmi
""")
toplevel("""
Vector.length (Vector.make 3. 4.);;
Shape.area (Shape.Square (Vector.make 0. 0., 3.));;
Shape.centre (Shape.Square (Vector.make 1. 1., 3.));;
""", args=("vector.cmo", "shape.cmo"))

Output

$ ocamlc -c shape.ml
File "shape.ml", line 3, characters 14-20:
3 |   | Circle of Vector.t * float (* centre and radius *)
                  ^^^^^^
Error: Unbound module Vector
[exit status 2]
$ ocamlc -c vector.ml
$ ocamlc -c shape.ml
$ ls
shape.cmi
shape.cmo
shape.ml
vector.cmi
vector.cmo
vector.ml
$ ocamlc -i shape.ml
type t = Circle of Vector.t * float | Square of Vector.t * float
val area : t -> float
val centre : t -> Vector.t
$ ocamlc -c my-shapes.ml
File "my-shapes.ml", line 1:
Warning 24 [bad-module-name]: bad source file name: My-shapes is not a valid module name.
$ ls *.cmi
my-shapes.cmi
shape.cmi
vector.cmi
$ ocaml vector.cmo shape.cmo
OCaml version 5.5.1
Enter #help;; for help.

# Vector.length (Vector.make 3. 4.);;
- : float = 5.
# Shape.area (Shape.Square (Vector.make 0. 0., 3.));;
- : float = 9.
# Shape.centre (Shape.Square (Vector.make 1. 1., 3.));;
- : Vector.t = {Vector.x = 2.5; y = 2.5}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 files_session.py

Compiled first, shape.ml stopped at line 3 with Unbound module Vector and exit status 2: nothing called Vector had been compiled yet. Once vector.ml was compiled, the same command passed, and each file left two results under its own base name: vector.cmi, the compiled interface that other files are checked against, and vector.cmo, the bytecode. ocamlc -i shape.ml printed the interface the compiler inferred for Shape, in which centre returns a Vector.t.

The manual states the rule: the compiler takes the source file’s base name, drops the folder and the suffix, and capitalises the first letter, so mylib/misc.ml is the module Misc. Each such file is a compilation unit: A source file compiled on its own into a module: vector.ml gives the module Vector, its compiled interface vector.cmi, against which other files are checked, and its compiled code vector.cmo., which the manual says behaves roughly like module Vector : sig ... end = struct ... end written around the file’s contents. ocaml vector.cmo shape.cmo started a toplevel with both units loaded, and they answered by name: Vector.length (Vector.make 3. 4.) is 5., a square of side 3 has area 9., and the centre of the square whose corner is at (1, 1) is {Vector.x = 2.5; y = 2.5}.

A file name that is not a module name

Wrong: my-shapes.ml. It compiled with Warning 24 [bad-module-name] and still wrote my-shapes.cmi, but no other file can write My-shapes, so the module cannot be used. Right: my_shapes.ml, the module My_shapes.

A file can also come with an interface file, vector.mli, that decides what other files may see of it; the next lesson, Signatures, abstract types and .mli files, is about those.

dune puts a library’s modules under the library’s name

In a dune project the same two files sit in lib/, whose dune file holds the stanza: One parenthesised declaration in a dune file, such as (library (name ledger)) or (test (name test_ledger) (libraries ledger)). More in “Your first dune project and editor setup” (library (name geometry)), and bin/main.ml lists geometry among its libraries. From outside the library, every module is reached through the library’s name:

bin/main.ml: the library used from outside ML · geometry/bin/main.ml
(* main.ml: outside the library, every module is reached through Geometry. *)
let () =
  let ring = Geometry.Shape.Circle (Geometry.Vector.make 0. 0., 2.) in
  let tile = Geometry.Shape.Square (Geometry.Vector.make 1. 1., 3.) in
  Printf.printf "circle area %.2f\n" (Geometry.Shape.area ring);
  Printf.printf "square area %.2f\n" (Geometry.Shape.area tile);
  print_endline (Geometry.Vector.to_string (Geometry.Shape.centre tile))
The geometry library built by dune Python · wrapped_session.py
from session import files, run

# The same two files as a dune library named geometry (examples/geometry/), used by bin/main.ml through the paths
# Geometry.Vector and Geometry.Shape. The listing shows the names dune gives the compiled modules, the module file
# geometry.ml-gen that dune writes for the library, and the command that compiles lib/shape.ml.
files(
    "geometry/dune-project",
    "geometry/lib/dune",
    "geometry/lib/vector.ml",
    "geometry/lib/shape.ml",
    "geometry/bin/dune",
    "geometry/bin/main.ml",
)
run("""
cd geometry
dune build
dune exec ./bin/main.exe
ls _build/default/lib/.geometry.objs/byte
cat _build/default/lib/geometry.ml-gen
dune rules lib/.geometry.objs/byte/geometry__Shape.cmo | tr -s ' \\n' ' ' | grep -o '(run [^)]*'
""")

Output

$ cd geometry
$ dune build
$ dune exec ./bin/main.exe
circle area 12.57
square area 9.00
(2.5, 2.5)
$ ls _build/default/lib/.geometry.objs/byte
geometry.cmi
geometry.cmo
geometry.cmt
geometry__Shape.cmi
geometry__Shape.cmo
geometry__Shape.cmt
geometry__Vector.cmi
geometry__Vector.cmo
geometry__Vector.cmt
$ cat _build/default/lib/geometry.ml-gen
(* generated by dune *)

(** @canonical Geometry.Shape *)
module Shape = Geometry__Shape

(** @canonical Geometry.Vector *)
module Vector = Geometry__Vector
$ dune rules lib/.geometry.objs/byte/geometry__Shape.cmo | tr -s ' \n' ' ' | grep -o '(run [^)]*'
(run ~/.opam/5.5.1/bin/ocamlc.opt -short-paths -keep-locs -warn-error +a -g -bin-annot -bin-annot-occurrences -I lib/.geometry.objs/byte -no-alias-deps -opaque -open Geometry -o lib/.geometry.objs/byte/geometry__Shape.cmo -c -impl lib/shape.ml

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 wrapped_session.py

The program printed circle area 12.57, square area 9.00 and the centre (2.5, 2.5), and dune worked out the compile order itself. The listing shows what it did with the files. vector.ml became the module Geometry__Vector and shape.ml Geometry__Shape, in files named geometry__Vector and geometry__Shape: the library’s name, two underscores, the module’s name. Then dune wrote one more module of its own, geometry.ml-gen, with the lines module Shape = Geometry__Shape and module Vector = Geometry__Vector, and compiled it as the module Geometry. The last command printed how shape.ml itself was compiled, with -open Geometry: that is why shape.ml can write Vector.t while bin/main.ml must write Geometry.Vector.t. Step through the whole path:

Interactive · stage by stage From two files to the path Geometry.Vector Step through what dune does with lib/vector.ml and lib/shape.ml, from the source files to the name a program outside the library writes.
From two files to the path Geometry.Vector, step 7 of 7. Showing Two files in lib/, One module per file (vector.cmo, shape.cmo), The module Geometry (not written), Geometry.Vector (Vector, Shape). In focus: One module per file, Geometry.Vector.Two files in lib/vector.ml and shape.mlOne module per filevector.cmo, shape.cmoThe module Geometrynot writtenGeometry.VectorVector, Shape

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

Every step as text
  1. Two files, one library. lib/ holds vector.ml and shape.ml, and lib/dune says (library (name geometry)). Nothing in either file mentions geometry: the library's name comes from the stanza alone.
  2. Each file becomes a module. dune compiles vector.ml as the module Geometry__Vector and shape.ml as Geometry__Shape, the library name, two underscores and the module name; the compiled files in .geometry.objs/byte start with geometry__.
  3. Short names inside. Every file of the library is compiled with -open Geometry. That is why shape.ml writes Vector.t and Vector.add without any prefix and still compiles.
  4. The wrapper module. dune also writes geometry.ml-gen, two aliases, module Shape = Geometry__Shape and module Vector = Geometry__Vector, and compiles it as the module Geometry, the one name meant for code outside the library.
  5. The path outside. bin/main.ml lists geometry in its libraries and writes Geometry.Shape.area and Geometry.Vector.make. The program printed circle area 12.57, square area 9.00 and (2.5, 2.5).
  6. No short names outside. A bin/main.ml that writes Vector alone does not compile: dune stopped with Unbound module Vector, because outside the library only Geometry is a top-level module.
  7. With (wrapped false). Adding (wrapped false) to lib/dune skips the wrapper: the compiled files are vector.cmo and shape.cmo, Vector and Shape become top-level modules, and the same short-name program printed (1, 2).

The library’s name has become a namespace for its modules, the way a module is a namespace for its values: OCaml.org’s guide to dune libraries says wrapping lets several modules share a name in one project as long as they sit in different libraries. The double underscore is chosen on purpose, the manual explains: when Lib.FooBar is an alias of Lib__FooBar, the compiler prints the dotted name, so messages about the library say Geometry.Vector. The next session shows the other side of wrapping, a program outside the library that uses the short names, before and after (wrapped false):

Short names from outside, before and after (wrapped false) Python · unwrapped_session.py
from session import files, run, write

# Outside a wrapped library its modules have no short names: a bin/main.ml that writes Vector alone does not compile.
# With (wrapped false) in lib/dune, dune writes no Geometry module and Vector and Shape become top-level modules.
files("geometry/dune-project", "geometry/lib/dune", "geometry/lib/vector.ml", "geometry/lib/shape.ml", "geometry/bin/dune")
write("geometry/bin/main.ml", "let () = print_endline (Vector.to_string (Vector.make 1. 2.))\n")
run("""
cd geometry
cat bin/main.ml
!dune build
""")
write("geometry/lib/dune", "(library\n (name geometry)\n (wrapped false))\n")
run("""
cd geometry
cat lib/dune
dune build
dune exec ./bin/main.exe
ls _build/default/lib/.geometry.objs/byte
""")

Output

$ cd geometry
$ cat bin/main.ml
let () = print_endline (Vector.to_string (Vector.make 1. 2.))
$ dune build
File "bin/main.ml", line 1, characters 24-30:
1 | let () = print_endline (Vector.to_string (Vector.make 1. 2.))
                            ^^^^^^
Error: Unbound module Vector
[exit status 1]
$ cd geometry
$ cat lib/dune
(library
 (name geometry)
 (wrapped false))
$ dune build
$ dune exec ./bin/main.exe
(1, 2)
$ ls _build/default/lib/.geometry.objs/byte
shape.cmi
shape.cmo
shape.cmt
vector.cmi
vector.cmo
vector.cmt

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 unwrapped_session.py

With wrapping on, Vector alone gave Error: Unbound module Vector. With (wrapped false) in lib/dune, dune wrote no Geometry module and kept the files’ own names, vector.cmo and shape.cmo, so Vector became a top-level module and the program printed (1, 2). dune’s documentation keeps wrapping as the default and calls it highly recommended: top-level module names must be unique when a program is linked, so a library that puts names such as Vector at the top level can collide with another library’s. It describes (wrapped false) as meant for libraries that already prefix their module names by hand, and for porting older projects to dune.

Key idea

Outside a dune library its modules are Library.Module; inside it they are plain Module, because dune compiles every file with -open of the library module it writes.

Checkpoint

Check: Each file is a module · dune's wrapped libraries

  1. Question 1 of 2 In a new folder, ocamlc -c shape.ml runs before vector.ml has ever been compiled. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It stops with Error: Unbound module Vector, because there is no compiled vector.cmi yet

    A file can be compiled only after the modules it names: the session's first ocamlc -c shape.ml failed with exit status 2, and the same command passed once vector.ml had been compiled.

  2. Question 2 of 2 bin/main.ml of the lesson's project needs the area of a shape tile from the wrapped geometry library. Which call compiles?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Geometry.Shape.area tile

    A wrapped library adds one top-level module named after the library, and its files are modules inside it: Geometry.Shape and Geometry.Vector.

Modules inside a file are reached by paths

A file can define modules of its own with module Name = struct ... end. units.ml holds two, Distance and Time, each with a type t and a conversion, and a function speed that takes one of each:

units.ml: two modules inside one file ML · units.ml
(* units.ml: one file, the module Units, with two modules of its own. *)
module Distance = struct
  type t = float

  let of_km km = km *. 1000.
  let to_string m = Printf.sprintf "%.0f m" m
end

module Time = struct
  type t = float

  let of_minutes m = m *. 60.
end

let speed (d : Distance.t) (t : Time.t) = d /. t

The session compiles it, prints the interface the compiler inferred, and uses its items from the toplevel:

Paths into units.ml, and an alias for one of them Python · paths_session.py
from session import files, run, toplevel

# units.ml (examples/units.ml) defines two modules, Distance and Time, so the file's module Units has a module inside
# it for each. ocamlc -i prints the nesting. The toplevel reaches each item by its full path, gives Units.Distance a
# second name with a module alias, and shows that Distance alone is not in scope.
files("units.ml")
run("""
ocamlc -c units.ml
ocamlc -i units.ml
""")
toplevel("""
Units.Distance.of_km 2.5;;
Units.Distance.to_string (Units.Distance.of_km 2.5);;
Units.speed (Units.Distance.of_km 6.) (Units.Time.of_minutes 25.);;
module D = Units.Distance;;
D.to_string (D.of_km 1.2);;
let lap : D.t = Units.Distance.of_km 0.4;;
Distance.of_km 1.;;
""", args=("units.cmo",))

Output

$ ocamlc -c units.ml
$ ocamlc -i units.ml
module Distance :
  sig
    type t = float
    val of_km : float -> float
    val to_string : float -> string
  end
module Time : sig type t = float val of_minutes : float -> float end
val speed : Distance.t -> Time.t -> float
$ ocaml units.cmo
OCaml version 5.5.1
Enter #help;; for help.

# Units.Distance.of_km 2.5;;
- : float = 2500.
# Units.Distance.to_string (Units.Distance.of_km 2.5);;
- : string = "2500 m"
# Units.speed (Units.Distance.of_km 6.) (Units.Time.of_minutes 25.);;
- : float = 4.
# module D = Units.Distance;;
module D = Units.Distance
# D.to_string (D.of_km 1.2);;
- : string = "1200 m"
# let lap : D.t = Units.Distance.of_km 0.4;;
val lap : D.t = 400.
# Distance.of_km 1.;;
Line 1, characters 0-8:
1 | Distance.of_km 1.;;
    ^^^^^^^^
Error: Unbound module Distance

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 paths_session.py

ocamlc -i printed the nesting: the interface of Units holds module Distance : sig ... end and module Time, and val speed : Distance.t -> Time.t -> float. From outside, a path names each step in turn, so Units.Distance.of_km 2.5 is the value of_km of the module Distance of the module Units: it gave 2500.. Units.speed (Units.Distance.of_km 6.) (Units.Time.of_minutes 25.) gave 4., 6,000 metres in 1,500 seconds.

Long paths are exact but heavy. module D = Units.Distance gives the module a second name, a module alias: A second name for an existing module, written module D = Units.Distance. The compiler treats both names as the same module, so D.t and Units.Distance.t are the same type.. The toplevel printed module D = Units.Distance instead of a copy of the signature, D.to_string (D.of_km 1.2) gave "1200 m", and lap, declared as a D.t, printed as 400.. The manual explains that the compiler treats the two paths as equal, and that with -no-alias-deps, the flag in dune’s compile command above, an alias adds no link-time dependency until code uses something through it. The last phrase shows what a path does not do for you: outside units.ml, Distance alone is Unbound module Distance. Follow the same paths in the figure:

Interactive · step by step Following the path Units.Distance.of_km Step through how each part of a path picks the next module, from the compiled file to the value, and where the alias D points.
Following the path Units.Distance.of_km, step 6 of 6. Showing units.cmi, Units, Units.Distance, Units.Time, module D. In focus: Units.Distance.read asaliasunits.cmiocamlc -c units.mlUnitsspeed, two modulesUnits.Distancet, of_km, to_stringUnits.Timet, of_minutesmodule D= Units.DistanceDistance alone: unbound

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

Every step as text
  1. The file is the module. ocamlc -c units.ml wrote units.cmi. When the toplevel, or another file, writes Units, the compiler reads that compiled interface: the module Units is the file units.ml.
  2. Two modules inside. units.ml defines Distance and Time with module ... = struct ... end, so Units holds two modules of its own, each with a type t, beside the function speed. ocamlc -i printed both inside the interface of Units.
  3. A path, one step at a time. Units.Distance.of_km names the module Units, then the module Distance inside it, then the value of_km. Units.Distance.of_km 2.5 gave 2500., and to_string turned that into "2500 m".
  4. Two paths in one call. Units.speed (Units.Distance.of_km 6.) (Units.Time.of_minutes 25.) gave 4.: 6,000 metres in 1,500 seconds is 4 metres a second. speed takes a Distance.t and a Time.t, as its type said.
  5. A second name. module D = Units.Distance makes D an alias: the toplevel printed module D = Units.Distance, not a copy of the signature. D.to_string (D.of_km 1.2) gave "1200 m", and a value written as D.t is a Units.Distance.t.
  6. No path, no module. Nothing opened Units, so Distance on its own is unknown outside units.ml: the toplevel answered Unbound module Distance. The full path or the alias reaches it.

Each step of a path picks a module inside the one before it, from the compiled file down to the value, and an alias starts the same walk from a shorter name.

A local open makes names short for one expression

Opening a module makes its names usable without the prefix. A local open: Opening a module for one expression only, written M.( ... ) or let open M in ..., so the names of M are short inside that expression and need the prefix everywhere else. does it for one expression only, in the two spellings the manual gives for the job: SMap.( ... ) and let open SMap in ..., which produce the same binding. SMap here is the module of maps with string keys that Map.Make (String) returns, as in the maps and sets lesson. Map.Make is 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”; the functors lesson explains how one works. The session defines SMap in defs.ml before the first prompt and loads units.cmo for the last two phrases:

Local opens in expressions and in a type Python · local_open_session.py
from session import files, run, toplevel, write

# SMap.( ... ) and let open SMap in ... make SMap's names short inside one expression only; an open SMap at the top
# level keeps them short to the end. Units.( ... ) works in a type as well (OCaml 5.2). defs.ml defines
# SMap = Map.Make (String) before the first prompt (-init prints nothing), and the toplevel loads units.cmo.
files("units.ml")
write("defs.ml", "module SMap = Map.Make (String)\n")
run("""
ocamlc -c units.ml
cat defs.ml
""")
toplevel("""
SMap.(empty |> add "pen" 3 |> add "ink" 2 |> bindings);;
let stock = let open SMap in empty |> add "pen" 3 |> add "ink" 2;;
SMap.(find "ink" stock, mem "nib" stock, cardinal stock);;
cardinal stock;;
open SMap let restock = add "nib" 10 stock;;
cardinal restock;;
type leg = Units.(Distance.t * Time.t);;
let warm_up : leg = Units.(Distance.of_km 1.2, Time.of_minutes 8.);;
""", args=("-init", "defs.ml", "units.cmo"))

Output

$ ocamlc -c units.ml
$ cat defs.ml
module SMap = Map.Make (String)
$ ocaml -init defs.ml units.cmo
OCaml version 5.5.1
Enter #help;; for help.

# SMap.(empty |> add "pen" 3 |> add "ink" 2 |> bindings);;
- : (SMap.key * int) list = [("ink", 2); ("pen", 3)]
# let stock = let open SMap in empty |> add "pen" 3 |> add "ink" 2;;
val stock : int SMap.t = <abstr>
# SMap.(find "ink" stock, mem "nib" stock, cardinal stock);;
- : int * bool * int = (2, false, 2)
# cardinal stock;;
Line 1, characters 0-8:
1 | cardinal stock;;
    ^^^^^^^^
Error: Unbound value cardinal
# open SMap let restock = add "nib" 10 stock;;
val restock : int SMap.t = <abstr>
# cardinal restock;;
- : int = 3
# type leg = Units.(Distance.t * Time.t);;
type leg = Units.Distance.t * Units.Time.t
# let warm_up : leg = Units.(Distance.of_km 1.2, Time.of_minutes 8.);;
val warm_up : leg = (1200., 480.)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 local_open_session.py

Inside SMap.( ... ) the names empty, add and bindings needed no prefix, and the pipeline gave [("ink", 2); ("pen", 3)], in key order. let open SMap in built the same map as stock, and the tuple SMap.(find "ink" stock, mem "nib" stock, cardinal stock) gave (2, false, 2). Outside both forms, cardinal stock was Unbound value cardinal: each open ended with its expression. A top-level open SMap lasts to the end of the file, or here of the session, so cardinal restock then worked and gave 3. When the expression already has brackets, the manual lets the parentheses go: SMap.[ ... ], SMap.[| ... |] and SMap.{ ... } open SMap inside a list, an array or a record.

Local opens in types: OCaml 5.2

Units.(Distance.t * Time.t) opens Units inside a type. The form was added in OCaml 5.2 (Changes, #12044), so code that uses it needs 5.2 or later; 5.5.1 accepted it and printed the type back with full paths, Units.Distance.t * Units.Time.t.

The last phrase used the same module both ways: the type leg from a local open in a type, and its value from a local open in an expression, Units.(Distance.of_km 1.2, Time.of_minutes 8.), which gave (1200., 480.). A local open keeps the short names next to the code that needs them, and its end shows the reader where they stop.

Checkpoint

Check: Nested modules, paths and aliases · Local opens in expressions and types

  1. Question 1 of 2 With units.cmo loaded, what does Units.Distance.of_km 0.75 return?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 750 metres

    The path names the module Units, then Distance inside it, then of_km, which multiplies by 1000: 0.75 km is 750 metres, printed as 750.

  2. Question 2 of 2 After let stock = let open SMap in empty |> add "pen" 3 |> add "ink" 2, the next phrase is cardinal stock. What does the toplevel answer?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Error: Unbound value cardinal

    let open SMap in e and SMap.(e) open SMap for e alone. Only a top-level open SMap keeps cardinal short for the rest of the file or session; the session then gave 3 for restock.

include copies a module’s contents, and open does not

extend.ml defines two modules that look alike. Its List starts with include Stdlib.List and adds sum; its Short starts with open Stdlib.List and defines the same sum:

extend.ml: include and open side by side ML · extend.ml
(* extend.ml: the module Extend. Its List is the standard List plus sum;
   its Short opens the standard List and defines the same sum. *)
module List = struct
  include Stdlib.List

  let sum l = fold_left ( + ) 0 l
end

module Short = struct
  open Stdlib.List

  let sum l = fold_left ( + ) 0 l
end

The session counts what each module exports and calls both:

What include and open each export Python · include_session.py
from session import files, run, toplevel, write

# extend.ml (examples/extend.ml) is the module Extend. Its List includes the standard List and adds sum; its Short
# opens the standard List and defines the same sum. ocamlc -i shows what each one exports: grep counts the values of
# the new List, then of a file that only includes the standard List, and finds both sums. The toplevel then calls
# both modules.
files("extend.ml")
write("only_list.ml", "include Stdlib.List\n")
run("""
ocamlc -c extend.ml
ocamlc -i extend.ml | grep -c '^    val'
cat only_list.ml
ocamlc -i only_list.ml | grep -c '^val'
ocamlc -i extend.ml | grep 'sum'
""")
toplevel("""
Extend.List.sum [72; 85; 64];;
Extend.List.length [72; 85; 64];;
Extend.Short.sum [72; 85; 64];;
Extend.Short.length [72; 85; 64];;
let open Extend in List.sum [1; 2; 3] + List.length [4; 5];;
""", args=("extend.cmo",))

Output

$ ocamlc -c extend.ml
$ ocamlc -i extend.ml | grep -c '^    val'
73
$ cat only_list.ml
include Stdlib.List
$ ocamlc -i only_list.ml | grep -c '^val'
72
$ ocamlc -i extend.ml | grep 'sum'
    val sum : int list -> int
module Short : sig val sum : int list -> int end
$ ocaml extend.cmo
OCaml version 5.5.1
Enter #help;; for help.

# Extend.List.sum [72; 85; 64];;
- : int = 221
# Extend.List.length [72; 85; 64];;
- : int = 3
# Extend.Short.sum [72; 85; 64];;
- : int = 221
# Extend.Short.length [72; 85; 64];;
Line 1, characters 0-19:
1 | Extend.Short.length [72; 85; 64];;
    ^^^^^^^^^^^^^^^^^^^
Error: Unbound value Extend.Short.length
# let open Extend in List.sum [1; 2; 3] + List.length [4; 5];;
- : int = 8

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 include_session.py

Both sums gave 221 for [72; 85; 64], because inside each module the names of List are short. The difference is what each module exports. grep found 73 values in the interface ocamlc -i printed for Extend.List: the 72 that the one-line file only_list.ml, include Stdlib.List, exports, plus sum. Extend.Short exports sum alone, and Extend.Short.length was Unbound value Extend.Short.length. The manual draws the line in one sentence: open only “provides short names” for what the opened module contains and defines nothing in the module being written, while include adds definitions.

That makes module List = struct include Stdlib.List ... end the usual way to extend a standard module. Inside let open Extend in, List means the extended one, so List.sum [1; 2; 3] + List.length [4; 5] used a new function and a standard one and gave 8. Writing Stdlib.List names the standard module plainly, whatever List means at that point of the file.

The five ways to bring a module closer differ in two things, how far the short names reach and whether the module you are writing exports anything new. Each row comes from this lesson’s sessions:

Form Short names reach Adds to what your module exports In this lesson
open SMap to the end of the file nothing cardinal restock gave 3
let open SMap in e e only nothing built stock
SMap.( e ) e only, and types since 5.2 nothing Units.(Distance.t * Time.t)
include Stdlib.List to the end of the structure every item of List Extend.List: 73 values
module D = Units.Distance nowhere: a second name the name D D.of_km 1.2

A rule of thumb

Write the qualified name when a module is used a few times, a local open when one expression uses it a lot, and a top-level open only for modules made to be opened, such as Printf; OCaml.org’s modules tutorial advises against opening List. Reach for include only when your module should export what it includes.

An open can hide a name you meant to use

An open puts every name of a module in front of the names already in scope, and some of those names may be ones you meant to use. The session below starts three toplevels: one that opens SMap at the top level, one with the default warnings, and one started with -w +44+45:

Opens that hide names, with and without warnings 44 and 45 Python · shadow_session.py
from session import files, run, toplevel, write

# Three toplevels. With SMap opened at the top level, compare is SMap.compare. Without warning options, an open that
# hides a name says nothing. With -w +44+45 the toplevel names the open that hides a value or a module (warning 44) or
# a constructor (warning 45); a qualified name opens nothing, and open! marks a hiding that is meant.
write("defs.ml", "module SMap = Map.Make (String)\n")
toplevel("""
open SMap let restock = add "nib" 10 empty;;
compare 3 4;;
Stdlib.compare 3 4;;
""", args=("-init", "defs.ml"))
toplevel("""
let tolerance = 0.01;;
module Physics = struct let g = 9.81 let tolerance = 1e-9 end;;
let close a b = let open Physics in Float.abs (a -. b) < tolerance;;
close 1.0 1.005;;
""")
files("extend.ml")
run("ocamlc -c extend.ml")
toplevel("""
let tolerance = 0.01;;
module Physics = struct let g = 9.81 let tolerance = 1e-9 end;;
let close a b = let open Physics in Float.abs (a -. b) < tolerance;;
let close a b = Float.abs (a -. b) < tolerance;;
close 1.0 1.005;;
type light = Red | Amber | Green;;
module Paint = struct type t = Red | Blue end;;
let c = Paint.(Red);;
let c = Paint.Red;;
let total = let open Extend in List.sum [72; 85; 64];;
let total = let open! Extend in List.sum [72; 85; 64];;
""", args=("-w", "+44+45", "extend.cmo"))

Output

$ ocaml -init defs.ml
OCaml version 5.5.1
Enter #help;; for help.

# open SMap let restock = add "nib" 10 empty;;
val restock : int SMap.t = <abstr>
# compare 3 4;;
Line 1, characters 8-9:
1 | compare 3 4;;
            ^
Error: The constant 3 has type int but an expression was expected of type
         'a -> 'a -> int
# Stdlib.compare 3 4;;
- : int = -1
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# let tolerance = 0.01;;
val tolerance : float = 0.01
# module Physics = struct let g = 9.81 let tolerance = 1e-9 end;;
module Physics : sig val g : float val tolerance : float end
# let close a b = let open Physics in Float.abs (a -. b) < tolerance;;
val close : float -> float -> bool = <fun>
# close 1.0 1.005;;
- : bool = false
$ ocamlc -c extend.ml
$ ocaml -w +44+45 extend.cmo
OCaml version 5.5.1
Enter #help;; for help.

# let tolerance = 0.01;;
val tolerance : float = 0.01
# module Physics = struct let g = 9.81 let tolerance = 1e-9 end;;
module Physics : sig val g : float val tolerance : float end
# let close a b = let open Physics in Float.abs (a -. b) < tolerance;;
Line 1, characters 20-32:
1 | let close a b = let open Physics in Float.abs (a -. b) < tolerance;;
                        ^^^^^^^^^^^^
Warning 44 [open-shadow-identifier]: this open statement shadows the
  value identifier tolerance (which is later used)

val close : float -> float -> bool = <fun>
# let close a b = Float.abs (a -. b) < tolerance;;
val close : float -> float -> bool = <fun>
# close 1.0 1.005;;
- : bool = true
# type light = Red | Amber | Green;;
type light = Red | Amber | Green
# module Paint = struct type t = Red | Blue end;;
module Paint : sig type t = Red | Blue end
# let c = Paint.(Red);;
Line 1, characters 8-13:
1 | let c = Paint.(Red);;
            ^^^^^
Warning 45 [open-shadow-label-constructor]: this open statement shadows the
  constructor Red (which is later used)

val c : Paint.t = Paint.Red
# let c = Paint.Red;;
val c : Paint.t = Paint.Red
# let total = let open Extend in List.sum [72; 85; 64];;
Line 1, characters 16-27:
1 | let total = let open Extend in List.sum [72; 85; 64];;
                    ^^^^^^^^^^^
Warning 44 [open-shadow-identifier]: this open statement shadows the
  module identifier List (which is later used)

val total : int = 221
# let total = let open! Extend in List.sum [72; 85; 64];;
val total : int = 221

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shadow_session.py

In the first toplevel, open SMap put SMap’s compare, which compares two maps, in front of Stdlib’s, so compare 3 4 was a type error: the constant 3 was expected to be a function of type 'a -> 'a -> int. Stdlib.compare 3 4 still gave -1. In the second, the program wants a tolerance of 0.01, defined at its top level, but close opens Physics to shorten a formula, and Physics has a tolerance of its own. Physics.tolerance shadowing: Hiding an earlier binding by binding the same name again. Code written before the new binding still refers to the old one. More in “Expressions, let bindings, if and scope” the 0.01, so close 1.0 1.005 gave false, and nothing said why. Step through the lookup that went wrong:

Interactive · step by step Which tolerance close reads Step through the lookup of the name tolerance inside close. Scopes are searched from the top down, and the first one that has the name wins.
Which tolerance close reads, step 6 of 6. Showing tolerance in close, close 1.0 1.005 (false, no warning), let open Physics, Warning 44, Top of the file, close, no open, Stdlib. In focus: close, no open, Top of the file.firstflagsthenreadsthentolerance in closeuses the nameclose 1.0 1.005false, no warninglet open Physicstolerance = 1e-9Warning 44with -w +44+45Top of the filetolerance = 0.01close, no openreads 0.01: trueStdlibopened in every file

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

Every step as text
  1. Two scopes to start with. The program defines tolerance = 0.01 at its top level, and every file starts with Stdlib opened. A name used inside close is looked up in the nearest scope first, then outwards.
  2. The open adds a scope. let open Physics in puts every name of Physics in a scope between the body of close and the rest of the file. Physics has a tolerance of its own, 1e-9.
  3. The first match wins. The lookup stops at the first scope that has the name. Physics.tolerance is found, so the 0.01 one scope further out is never read.
  4. A wrong answer, in silence. close 1.0 1.005 compared a difference of 0.005 with 1e-9 and gave false. The toplevel printed no warning at all: warnings 44 and 45 are off unless they are switched on.
  5. Warning 44 names the open. Started with -w +44+45, the toplevel flagged the open: this open statement shadows the value identifier tolerance (which is later used). It still accepted close, which reads 1e-9 as before.
  6. The fix reads 0.01. close written without the open has no Physics scope in its way, so it reads the 0.01 of the file: close 1.0 1.005 gave true, with no warning. Physics.g written in full still reaches the other module.

The compiler stays quiet because warnings 44 and 45 are off by default: the default warning setting the manual gives includes -44-45. Warning 44 reports an open that shadows a value, a type or a module that is later used; warning 45 does the same for a label or a constructor. The third toplevel named each case: tolerance, the constructor Red that Paint.(Red) took from Paint instead of the type light, and the module List that let open Extend in replaced. The qualified forms drew no warning: close without the open gave true, and Paint.Red printed nothing. open! is the third answer. The manual calls the hiding it marks “intentional”: the ! tells the compiler not to warn, so let open! Extend in gave 221 in silence.

An open at the top of a file

Wrong: open SMap as the first line, then compare 3 4 or List.sort compare xs further down, where compare is now SMap.compare. Right: SMap.compare or Stdlib.compare written out, or a local open around only the expressions that work on maps.

In a dune project the same opens build without a word. The compile command that the wrapped session printed shows the flags of dune’s default dev profile: -warn-error +a, which turns every warning that is on into an error, but no -w option, so 44 and 45 stay off. This project’s bin/main.ml opens Physics by mistake in close and Extend on purpose with open!:

shadow/bin/main.ml: one open by mistake, one on purpose ML · shadow/bin/main.ml
(* main.ml: one open hides a name by accident, the other on purpose. *)
let tolerance = 0.01

module Physics = struct
  let g = 9.81
  let tolerance = 1e-9
end

(* Meant to compare with the 0.01 above. *)
let close a b =
  let open Physics in
  Float.abs (a -. b) < tolerance

(* Extend.List is the standard List plus sum, meant to replace it here. *)
let total marks =
  let open! Extend in
  List.sum marks

let () =
  Printf.printf "close %b, total %d, g %g\n" (close 1.0 1.005) (total [ 72; 85; 64 ]) Physics.g
Warnings 44 and 45 switched on in a dune build Python · shadow_dune_session.py
from session import files, run

# The same two opens in a dune project (examples/shadow/): bin/main.ml opens Physics inside close by accident and
# opens Extend on purpose with open!. The dev profile builds it without a word. With -w +44+45 added to the
# executable's flags, the build stops at the accidental open only. Then bin/main.ml is replaced by main_fixed.ml,
# whose close opens nothing, and the build passes.
files(
    "shadow/dune-project",
    "shadow/bin/dune",
    "shadow/bin/main.ml",
    "extend.ml=shadow/bin/extend.ml",
    "shadow/main_fixed.ml",
)
run("""
cd shadow
dune build
dune exec ./bin/main.exe
printf '(executable\\n (name main)\\n (flags (:standard -w +44+45)))\\n' > bin/dune
cat bin/dune
!dune build
cp main_fixed.ml bin/main.ml
grep -n 'open' bin/main.ml
dune build
dune exec ./bin/main.exe
""")

Output

$ cd shadow
$ dune build
$ dune exec ./bin/main.exe
close false, total 221, g 9.81
$ printf '(executable\n (name main)\n (flags (:standard -w +44+45)))\n' > bin/dune
$ cat bin/dune
(executable
 (name main)
 (flags (:standard -w +44+45)))
$ dune build
File "bin/main.ml", line 11, characters 6-18:
11 |   let open Physics in
           ^^^^^^^^^^^^
Error (warning 44 [open-shadow-identifier]): this open statement shadows the
  value identifier tolerance (which is later used)
[exit status 1]
$ cp main_fixed.ml bin/main.ml
$ grep -n 'open' bin/main.ml
14:  let open! Extend in
$ dune build
$ dune exec ./bin/main.exe
close true, total 221, g 9.81

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shadow_dune_session.py

The default build printed close false, total 221, g 9.81. With (flags (:standard -w +44+45)) in bin/dune, the dev profile’s -warn-error +a made the warning an error: the build stopped at line 11, let open Physics in, with Error (warning 44 [open-shadow-identifier]), and said nothing about the let open! Extend in on line 16. With close rewritten without the open, from main_fixed.ml, the build passed and printed close true.

Key idea

An open puts every name of a module in front of yours; switch on warnings 44 and 45 to see what it hides, then write the qualified name, or open! where the hiding is the point.

Checkpoint

Check: include and open compared · Shadowing by open, warnings 44 and 45

  1. Question 1 of 2 In extend.ml, Short starts with open Stdlib.List and defines sum. What does the module Extend.Short export?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: sum alone

    include copies the definitions of a module into the one being written, open only shortens names. Extend.List exported 73 values, Extend.Short exported one.

  2. Question 2 of 2 close used let open Physics in Float.abs (a -. b) < tolerance and read the tolerance of Physics. Which change makes close read the top-level 0.01, with -w +44+45 still quiet?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Remove the open, so the expression reads the file's own tolerance

    A name is looked up in the nearest scope first, and a local open is nearer than anything at the top level. The fix is to stop opening Physics there, or to write the qualified name of the value you want.

Practise: files, paths and opens

The questions rename files, add functions to Extend.List and move opens around in the lesson’s own code. Decide each answer from the rule it tests before you check it:

Practice that adapts to you

Practise this lesson

18 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 18 You compile lib/report_card.ml with ocamlc -c. Under which module name do other files use it?

    Choose one answer.

    Show the answer: Question 1 of 18

    Answer: Report_card

    The manual's rule: the module name is the capitalised base name of the source file, as mylib/misc.ml gives Misc.

  2. Question 2 of 18 When shape.ml names Vector.t, which file does the compiler read to check it?

    Choose one answer.

    Show the answer: Question 2 of 18

    Answer: vector.cmi, the compiled interface of Vector

    Each compiled module leaves a .cmi and a .cmo with the source file's base name; the .cmi is the one the type checker reads.

  3. Question 3 of 18 In the dune project, a teammate renames lib/vector.ml to lib/point.ml and changes nothing else. What does dune build say?

    Choose one answer.

    Show the answer: Question 3 of 18

    Answer: lib/shape.ml fails with Error: Unbound module Vector

    Renaming a file renames its module, so every file that uses the old name must change: here shape.ml and main.ml.

  4. Question 4 of 18 In the wrapped library geometry, which name did the compiled files of lib/shape.ml carry?

    Choose one answer.

    Show the answer: Question 4 of 18

    Answer: geometry__Shape

    dune compiles every file of a wrapped library as Library__Module and writes a module named after the library that gives each one its short name.

  5. Question 5 of 18 Inside the wrapped library, why can lib/shape.ml write Vector.add with no Geometry. in front?

    Choose one answer.

    Show the answer: Question 5 of 18

    Answer: dune compiles every file of the library with -open Geometry

    -open Geometry acts like an open at the top of each file of the library, which is why inside the library the short names work and outside it they do not.

  6. Question 6 of 18 A team sets (wrapped false) so that callers can write Vector instead of Geometry.Vector. Which risk does dune's library documentation give for that choice?

    Choose one answer.

    Show the answer: Question 6 of 18

    Answer: Every module becomes a top-level name, which can clash with a module of the same name in another library of the program

    Wrapping gives every library a namespace of its own, so two libraries may both have a vector.ml. Without it, the module names of all libraries share one space.

  7. Question 7 of 18 From outside units.ml, which expression converts 25 minutes to seconds?

    Choose one answer.

    Show the answer: Question 7 of 18

    Answer: Units.Time.of_minutes 25.

    A path follows the nesting exactly: each module name selects a module inside the one before it.

  8. Question 8 of 18 What does Units.speed (Units.Distance.of_km 3.) (Units.Time.of_minutes 10.) return?

    Type a number.

    Show the answer: Question 8 of 18

    Answer: 5 metres a second

    of_km 3. is 3000 metres and of_minutes 10. is 600 seconds, and speed divides the first by the second: 3000 / 600 = 5.

  9. Question 9 of 18 A file uses Units.Distance twenty times. Which change shortens those uses while hiding none of the names the file already has?

    Choose one answer.

    Show the answer: Question 9 of 18

    Answer: module D = Units.Distance at the top, then D.of_km and D.to_string

    A module alias is a second name for the same module: short to write and without the side effects of an open.

  10. Question 10 of 18 Which of these give the same result as SMap.(cardinal stock)?

    Choose every answer that is right.

    Show the answer: Question 10 of 18

    Answer:

    • let open SMap in cardinal stock
    • SMap.cardinal stock

    Local opens and qualified names are three spellings of the same access; the bracket forms keep the brackets' own meaning.

  11. Question 11 of 18 stock holds pen and ink. What does SMap.(stock |> add "nib" 10 |> remove "pen" |> cardinal) give?

    Type a number.

    Show the answer: Question 11 of 18

    Answer: 2

    Inside the local open every name is SMap's: adding nib makes three keys and removing pen leaves two, ink and nib.

  12. Question 12 of 18 A project must still build with OCaml 5.1. Which way of writing the type of a leg keeps that possible?

    Choose one answer.

    Show the answer: Question 12 of 18

    Answer: type leg = Units.Distance.t * Units.Time.t

    The type form Units.( ... ) is shorthand for the full paths, and it is new in 5.2; code that must build on older compilers writes the paths out.

  13. Question 13 of 18 What does include M give the module being written that open M does not?

    Choose one answer.

    Show the answer: Question 13 of 18

    Answer: Every definition of M, as items the new module exports

    open only changes how names are looked up; include adds definitions to the module, which is why it is used to extend modules.

  14. Question 14 of 18 Extend.List exported 73 values. If a second function, average, were added after sum, how many values would ocamlc -i count?

    Type a number.

    Show the answer: Question 14 of 18

    Answer: 74 values

    The 72 values that include brought from the standard List, plus sum and average, which the module defines itself.

  15. Question 15 of 18 You want String.is_blank next to every function of the standard String, in the files that ask for it. Which shape works?

    Choose one answer.

    Show the answer: Question 15 of 18

    Answer: A module Ext with module String = struct include Stdlib.String let is_blank s = ... end, opened with open! Ext where needed

    To extend a module, write a new module that includes it and adds to it, then decide where its name should replace the standard one.

  16. Question 16 of 18 Which warning reports an open that hides a constructor, such as the Red that Paint.(Red) took from Paint?

    Choose one answer.

    Show the answer: Question 16 of 18

    Answer: Warning 45, open-shadow-label-constructor

    Both shadowing warnings date from OCaml 4.01 and are off by default: 44 for identifiers, 45 for labels and constructors.

  17. Question 17 of 18 let open! Extend in List.sum [72; 85; 64] gave 221 with no warning under -w +44+45. Which List did it use?

    Choose one answer.

    Show the answer: Question 17 of 18

    Answer: Extend.List: open! keeps the hiding and only stops warning 44

    The ! changes the warning, not the meaning: write it when the hiding is the reason for the open.

  18. Question 18 of 18 A file starts with open SMap and later sorts a list of integers with List.sort compare xs. What happens?

    Choose one answer.

    Show the answer: Question 18 of 18

    Answer: It does not compile: compare is SMap.compare there, a function that compares two maps

    A top-level open hides names for the rest of the file. Write Stdlib.compare, or open SMap only around the code that needs it.

Interview questions

Warm-up (fresher to 2–5 years): how do open and include differ, and when do you reach for each?

open M lets the following code use M’s names without the M. prefix, to the end of the structure, or for one expression as a local open, M.( ... ) or let open M in. It defines nothing, so a module that opens List exports none of List’s functions. include M copies every definition of M into the module being written, so they become part of what it exports: a module List that includes Stdlib.List and adds sum exports all of List’s values plus sum. Use open, ideally a local open, to shorten names in code you write; use include to extend or re-export a module. Both can hide names already in scope, so for a name used only a few times the qualified form is the safest.

The two questions below go further: one about two files of a library that each need the other, one about two libraries that each bring their own util.ml. For each, decide which output you would read first before you look at its first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    In a dune library, order.ml calls Customer.name to label an order and customer.ml calls Order.total to add up what a customer spent. dune stops with "Error: Dependency cycle between" the two modules. Why can OCaml not build these files as they are, and how would you restructure them?

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

  2. Mid to senior

    Two internal libraries, maps and billing, were ported to dune with (wrapped false), and each has a util.ml. A program that links both fails with "make inconsistent assumptions over interface Util". What is wrong, and how would you fix it without breaking callers that still write Route and Invoice with no prefix?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

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

  1. Which module does the file my_shapes.ml define, and why does my-shapes.ml fail?

    My_shapes: every .ml file is a module named after the file with its first letter capitalised. my-shapes.ml compiles with warning 24, but no other file can name the module.

  2. Why can shape.ml stop with Error: Unbound module Vector?

    A file compiles only once the modules it names have compiled interfaces, so compiling shape.ml before vector.ml fails.

  3. What are a dune library's modules called inside the library and outside it?

    Outside, Geometry.Vector and Geometry.Shape; inside, plain Vector and Shape, because dune opens the library module it generates. (wrapped false) puts them at the top level.

  4. How is a module defined inside a file reached, and how do you shorten its name?

    By a path such as Units.Distance.of_km. module D = Units.Distance gives a shorter name that the compiler treats as the same module.

  5. How far do open M, let open M in e and M.( e ) reach?

    open M to the end of the file; the two local forms stop at the end of one expression. Since OCaml 5.2, M.( … ) also works inside types.

  6. How do include and open differ?

    include copies a module's definitions into the module being written, so they are exported: Extend.List exports all of List plus sum. open only shortens names and exports nothing.

  7. What can a top-level open SMap silently do to a later compare 3 4?

    Make it mean SMap.compare. Warnings 44 and 45 report such hiding once switched on, open! marks a hiding you intend, and a qualified name such as Stdlib.compare avoids the question.

  8. When is a qualified name, a local open or a top-level open the better choice?

    A qualified name when a module is used a few times, a local open when one expression uses it a lot, and a top-level open only for modules made to be opened, such as Printf.

Key takeaways

  • Every .ml file is a module named after the file with its first letter capitalised, and a file compiles only once the modules it names have compiled interfaces.
  • A dune library wraps its files: outside it they are Geometry.Vector and Geometry.Shape, inside it plain Vector and Shape, and (wrapped false) puts them at the top level under their own names.
  • A module defined inside a file is reached by a path such as Units.Distance.of_km, and module D = Units.Distance gives it a shorter name that the compiler treats as the same module.
  • open M, let open M in e and M.( e ) all make M’s names short; the local forms stop at the end of one expression, and since OCaml 5.2 M.( ... ) also works in types.
  • include copies a module’s definitions into the module being written, which is how Extend.List exports all of List plus sum, while open exports nothing.
  • An open can hide a name you meant to use without a warning; warnings 44 and 45 report it once switched on, open! marks a hiding you intend, and a qualified name avoids the question.

Terms in this lesson

compilation unit
A source file compiled on its own into a module: vector.ml gives the module Vector, its compiled interface vector.cmi, against which other files are checked, and its compiled code vector.cmo.
local open
Opening a module for one expression only, written M.( ... ) or let open M in ..., so the names of M are short inside that expression and need the prefix everywhere else.
module
A named collection of definitions (values, types, exceptions and other modules), reached from outside with a dot, as in List.length. Every .ml file is a module named after the file.
module alias
A second name for an existing module, written module D = Units.Distance. The compiler treats both names as the same module, so D.t and Units.Distance.t are the same type.

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.