On this page:
3.1 Why Modules?
3.2 Creating a Module
3.3 Scope and the Dot Operator
3.3.1 Opening a Module
3.3.2 Opening Multiple Modules
3.3.3 Local Opens
3.4 Module Signatures
3.4.1 Implementing a Signature
3.5 The Stack Signature
3.6 Abstract Types
3.6.1 A Second Implementation:   Variant  Stack
3.6.2 Functional Data Structures
3.7 Case Study:   A Fraction Module with an Invariant
3.8 Modules and Files
3.8.1 Loading Compiled Modules into the Toplevel
3.9 Extending a Module with include
3.10 Exercises
3.10.1 Project:   A Store Inventory Library
3.11 Summary
9.1

3 OCaml Modules🔗

    3.1 Why Modules?

    3.2 Creating a Module

    3.3 Scope and the Dot Operator

    3.4 Module Signatures

    3.5 The Stack Signature

    3.6 Abstract Types

    3.7 Case Study: A Fraction Module with an Invariant

    3.8 Modules and Files

    3.9 Extending a Module with include

    3.10 Exercises

    3.11 Summary

By the end of this chapter, you should be able to:
  • Explain why large programs are organized into modules.

  • Define OCaml modules with module ... = struct ... end and access their members with the dot operator.

  • Control name visibility with open, local opens, and explain shadowing when multiple modules are opened.

  • Write module signatures with module type ... = sig ... end and use them to hide values and types.

  • Use abstract types to enforce data structure invariants and enable multiple implementations of the same interface.

  • Organize a multi-file OCaml program using .ml and .mli files and compile it with ocamlc or dune.

3.1 Why Modules?🔗

Real software is big. Some examples:
  • Windows Vista: about 50 million lines of code

  • Mac OS X 10.4 (Tiger): about 86 million lines of code

  • Google’s codebase: about 2 billion lines of code

When a program is small enough, we can keep all of its details in one file and hold the whole design in our heads. Real application programs are simply too large and too complex for that. To manage this complexity, we practice modular programming: the code is composed of many separate modules that are developed and understood independently.

A module groups together associated types, functions, and data. Each module has an interface that describes what it provides to its clients, while its implementation details stay hidden inside. You have already been using modules: the OCaml standard library is organized as a collection of modules such as List, String, and Printf. When you write List.filter, you are calling the filter function of the List module.

In this chapter we use two running examples:
  • Stack — a classic data structure, which we will implement in two different ways behind the same interface.

  • Fraction — a data type with an invariant (fractions are always stored in reduced form) that modules help us protect.

3.2 Creating a Module🔗

Modules in OCaml are created by module definitions with the following syntax:

module ModuleName = struct
  (* definitions *)
end

The definitions inside struct ... end may be anything we can write at the top level: let bindings, type definitions, exception definitions, even nested modules. Module names must begin with an uppercase letter.

Here is our first version of a stack module. It represents a stack as an OCaml list, where the head of the list is the top of the stack:

OCaml REPL

module ListStack = struct 
    let empty = [] 
 
    let is_empty s = (s = []) 
 
    let push x s = x :: s 
 
    let peek = function 
      | []   -> failwith "Empty" 
      | x::_ -> x 
 
    let pop = function 
      | []    -> failwith "Empty" 
      | _::xs -> xs 
 end;;

[Output]
module ListStack :
  sig
    val empty : 'a list
    val is_empty : 'a list -> bool
    val push : 'a -> 'a list -> 'a list
    val peek : 'a list -> 'a
    val pop : 'a list -> 'a list
  end

Notice that the toplevel replies with the module’s signature: the names and types of everything defined inside it. Recall that let peek = function ... is shorthand for let peek x = match x with ....

3.3 Scope and the Dot Operator🔗

After a module has been defined, you can access the names within it using the . (dot) operator:

OCaml REPL

module M = struct 
    let x = 42 
 end;; 
 
 M.x;;

[Output]
module M : sig val x : int end
- : int = 42

3.3.1 Opening a Module🔗

Prefixing every use with the module name can get verbose. Opening a module brings all of its definitions into the current scope:

OCaml REPL

module M = struct 
    let x = 42 
 end;; 
 
 x;; 
 
 open M;; 
 
 x;;

[Output]
module M : sig val x : int end
Line 2, characters 1-2:
2 |  x;; 
     ^
Error: Unbound value x
- : int = 42

Before open M, the name x is unbound and OCaml reports an error; after the open, x refers to M.x.

3.3.2 Opening Multiple Modules🔗

If two modules both define the same name and you open both of them, names opened later shadow names opened earlier:

OCaml REPL

module M = struct 
    let x = 42 
 end;; 
 
 module N = struct 
    let x = "CMSC330" 
 end;; 
 
 open M;; 
 open N;; 
 
 x;;

[Output]
module M : sig val x : int end
module N : sig val x : string end
- : string = "CMSC330"

Quiz: After open M followed by open N above, what is x?

Answer: "CMSC330". Since N is opened after M, its definition of x shadows the one from M. Shadowing is not an error in OCaml — the most recent binding wins.

3.3.3 Local Opens🔗

Opening a module at the top of a file pollutes the entire scope. To limit an open to a single expression, OCaml provides local opens. Compare:

Without opening List:

let f x =
  let y = List.filter ((>) 0) x in ...

Opening List locally:

let f x =
  let open List in
  let y = filter ((>) 0) x in ...

Inside the body of the let open, the name filter is bound to List.filter. The open ends where the expression ends, so the rest of the program is unaffected. An equivalent, even more compact form is List.(filter ((>) 0) x).

3.4 Module Signatures🔗

A signature is the interface of a structure. A signature specifies which components of a structure are accessible from the outside, and with which types. Signatures are written with module type:

module type SIGNAME = sig
  (* val and type declarations *)
end

A signature can be used to hide some components of a structure, such as local helper functions. In the following example, the signature FOO only declares add, so the mult function of Foo is not accessible from outside:

OCaml REPL

module type FOO = sig 
    val add : int -> int -> int 
 end;; 
 
 module Foo : FOO = struct 
    let add x y = x + y 
    let mult x y = x * y 
 end;; 
 
 Foo.add 3 4;; 
 
 Foo.mult 3 4;;

[Output]
module type FOO = sig val add : int -> int -> int end
module Foo : FOO
- : int = 7
Line 2, characters 1-9:
2 |  Foo.mult 3 4;;
     ^^^^^^^^
Error: Unbound value Foo.mult

The call Foo.add 3 4 succeeds, but Foo.mult 3 4 is rejected: mult exists inside the module, yet the signature makes it invisible to clients. A few things to know about signatures:

  • Convention: signature names are often written in all-caps (this is a convention, not a requirement).

  • Items can be omitted from a signature. This is what gives us the ability to hide values.

  • The default signature of a module hides nothing. If you enter a module with no signature ascription, OCaml infers a signature that exposes everything — exactly what the toplevel printed back for ListStack above.

3.4.1 Implementing a Signature🔗

A module M : Sig must provide every name declared in Sig, with types at least as general as the declared ones. The module may contain more definitions than the signature declares (they are simply hidden), but never fewer.

OCaml REPL

module type Sig = sig 
    val f : int -> int 
 end;; 
 
 module M1 : Sig = struct 
    let f x = x + 1 
 end;; 
 
 module M2 : Sig = struct 
    let f x = x 
 end;; 
 
 M2.f;;

[Output]
module type Sig = sig val f : int -> int end
module M1 : Sig
module M2 : Sig
- : int -> int = <fun>

M1.f has exactly the type int -> int that the signature demands. M2.f on its own has the polymorphic type a -> a, which is more general than int -> int, so it is safe to use it where an int -> int is required. Note the effect of the ascription: outside the module, M2.f has type int -> int — the signature is what clients see, not the inferred type.

3.5 The Stack Signature🔗

Let us give our stack a proper interface. Here is a signature for stacks, including the type of stacks themselves:

module type Stack = sig
  type 'a stack = 'a list
  val empty    : 'a stack
  val is_empty : 'a stack -> bool
  val push     : 'a -> 'a stack -> 'a stack
  val peek     : 'a stack -> 'a
  val pop      : 'a stack -> 'a stack
end

The declaration type a stack = a list says: a stack of as is a list of as. We can now ascribe this signature to our module and use it:

OCaml REPL

module type Stack = sig 
    type 'a stack = 'a list 
    val empty    : 'a stack 
    val is_empty : 'a stack -> bool 
    val push     : 'a -> 'a stack -> 'a stack 
    val peek     : 'a stack -> 'a 
    val pop      : 'a stack -> 'a stack 
 end;; 
 
 module ListStack : Stack = struct 
    type 'a stack = 'a list 
    let empty = [] 
    let is_empty s = (s = []) 
    let push x s = x :: s 
    let peek = function 
      | []   -> failwith "Empty" 
      | x::_ -> x 
    let pop = function 
      | []    -> failwith "Empty" 
      | _::xs -> xs 
 end;; 
 
 let t = ListStack.empty;; 
 let t2 = ListStack.push 10 t;; 
 let t3 = ListStack.push 20 t2;; 
 ListStack.peek t3;; 
 let t4 = ListStack.pop t3;;

[Output]
module type Stack =
  sig
    type 'a stack = 'a list
    val empty : 'a stack
    val is_empty : 'a stack -> bool
    val push : 'a -> 'a stack -> 'a stack
    val peek : 'a stack -> 'a
    val pop : 'a stack -> 'a stack
  end
module ListStack : Stack
val t : 'a ListStack.stack = []
val t2 : int ListStack.stack = [10]
val t3 : int ListStack.stack = [20; 10]
- : int = 20
val t4 : int ListStack.stack = [10]

Look carefully at the toplevel’s answers: the values of t2 and t3 are printed as lists like [10] and [20; 10]. Because the signature made the type equal to a list, the representation is visible to the user. Clients can see — and worse, exploit — the fact that a stack is a list. Nothing stops a client from writing [1;2;3] |> ListStack.peek or pattern matching a stack against x::xs, which defeats the purpose of the interface.

3.6 Abstract Types🔗

We can fix this by making the type abstract: declare the type in the signature without giving its definition.

module type Stack = sig
  type 'a stack            (* type is abstract *)
  val empty    : 'a stack
  val is_empty : 'a stack -> bool
  val push     : 'a -> 'a stack -> 'a stack
  val peek     : 'a stack -> 'a
  val pop      : 'a stack -> 'a stack
end

A module that implements Stack must
  • specify a concrete type for the abstract type a stack, and

  • define all the names declared in the signature.

Clients, on the other hand, can only create and manipulate stacks through the functions listed in the signature. Let us repeat the earlier session with the abstract signature:

OCaml REPL

module type Stack = sig 
    type 'a stack 
    val empty    : 'a stack 
    val is_empty : 'a stack -> bool 
    val push     : 'a -> 'a stack -> 'a stack 
    val peek     : 'a stack -> 'a 
    val pop      : 'a stack -> 'a stack 
 end;; 
 
 module ListStack : Stack = struct 
    type 'a stack = 'a list 
    let empty = [] 
    let is_empty s = (s = []) 
    let push x s = x :: s 
    let peek = function 
      | []   -> failwith "Empty" 
      | x::_ -> x 
    let pop = function 
      | []    -> failwith "Empty" 
      | _::xs -> xs 
 end;; 
 
 let t = ListStack.empty;; 
 let t2 = ListStack.push 10 t;; 
 let t3 = ListStack.push 20 t2;; 
 ListStack.peek t3;; 
 let t4 = ListStack.pop t3;;

[Output]
module type Stack =
  sig
    type 'a stack
    val empty : 'a stack
    val is_empty : 'a stack -> bool
    val push : 'a -> 'a stack -> 'a stack
    val peek : 'a stack -> 'a
    val pop : 'a stack -> 'a stack
  end
module ListStack : Stack
val t : 'a ListStack.stack = <abstr>
val t2 : int ListStack.stack = <abstr>
val t3 : int ListStack.stack = <abstr>
- : int = 20
val t4 : int ListStack.stack = <abstr>

The implementation is not visible anymore: every stack value is printed as <abstr>. The only way to build or inspect a stack is through empty, push, peek, pop, and is_empty. This is data abstraction, the same idea as private fields plus a public interface in object-oriented languages — but enforced by the type system at the module boundary.

3.6.1 A Second Implementation: VariantStack🔗

Because clients can no longer depend on the representation, we are free to implement the same signature with a completely different data type. Here is a stack built from a custom variant type instead of a list:

OCaml REPL

module type Stack = sig 
    type 'a stack 
    val empty    : 'a stack 
    val is_empty : 'a stack -> bool 
    val push     : 'a -> 'a stack -> 'a stack 
    val peek     : 'a stack -> 'a 
    val pop      : 'a stack -> 'a stack 
 end;; 
 
 module VarStack : Stack = struct 
    type 'a stack = 
      | Empty 
      | Entry of 'a * 'a stack 
 
    let empty = Empty 
    let is_empty s = (s = Empty) 
    let push x s = Entry (x, s) 
    let peek = function 
      | Empty      -> failwith "Empty" 
      | Entry(x,_) -> x 
    let pop = function 
      | Empty      -> failwith "Empty" 
      | Entry(_,s) -> s 
 end;; 
 
 let t = VarStack.push 20 (VarStack.push 10 VarStack.empty);; 
 VarStack.peek t;;

[Output]
module type Stack =
  sig
    type 'a stack
    val empty : 'a stack
    val is_empty : 'a stack -> bool
    val push : 'a -> 'a stack -> 'a stack
    val peek : 'a stack -> 'a
    val pop : 'a stack -> 'a stack
  end
module VarStack : Stack
val t : int VarStack.stack = <abstr>
- : int = 20

ListStack and VarStack implement the same signature with different representations. Any code written against the Stack signature works with either one, and we can swap implementations (say, for performance) without touching client code. This is the key payoff of signatures: one interface, many implementations.

Quiz: With the abstract Stack signature, which of the following expressions type checks?

Answer: only ListStack.peek (ListStack.push 1 ListStack.empty). Since a stack is abstract, a plain list is not a stack (rules out the first and third options), and ListStack and VarStack have distinct abstract types, so a ListStack.stack cannot be passed to VarStack.peek.

3.6.2 Functional Data Structures🔗

Our stacks are immutable, persistent data structures: updating the data structure with one of its operations does not change the existing version, but instead produces a new version.

open ListStack
let s  = empty
let s2 = push 10 s
let s3 = push 20 s2

Pushing onto s2 does not destroy s2: after the third line, both s2 (one element) and s3 (two elements) exist. Both versions persist and can be used independently. This is the normal way of the functional world — the same reason appending to a list creates a new list — and it makes reasoning about programs easier because values never change behind your back.

3.7 Case Study: A Fraction Module with an Invariant🔗

Abstraction is not just about hiding helper functions; it lets a module enforce invariants on its data. Consider a module for fractions with the invariant that fractions are always stored in reduced form (e.g., 2/8 is stored as 1/4).

First, a version whose signature exposes the representation:

OCaml REPL

module type FRACTION = sig 
    type fraction = Frac of int * int 
    exception BadFrac 
    val make : int * int -> fraction 
    val add : fraction * fraction -> fraction 
    val toString : fraction -> string 
 end;; 
 
 module Fraction : FRACTION = struct 
    type fraction = Frac of int * int 
    exception BadFrac 
 
    (* gcd and reduce are not in the signature: hidden helpers *) 
    let rec gcd (x,y) = 
      let (x,y) = if x >= y then (x,y) else (y,x) in 
      if y = 0 then x else gcd (y, x mod y) 
 
    let reduce (Frac(x,y)) = 
      let d = gcd (x,y) in 
      Frac (x/d, y/d) 
 
    (* the denominator cannot be 0 *) 
    let make (x,y) = 
      if y = 0 then raise BadFrac 
      else reduce (Frac(x,y)) 
 
    let add (r1,r2) = 
      match (r1,r2) with 
        (Frac(a,b), Frac(c,d)) -> reduce (Frac(a*d + b*c, b*d)) 
 
    let toString (Frac(a,b)) = 
      if b = 1 then string_of_int a 
      else if a = 0 then "0" 
      else string_of_int a ^ "/" ^ string_of_int b 
 end;; 
 
 let f1 = Fraction.make (2,8);; 
 let f2 = Fraction.make (25,100);; 
 Fraction.toString (Fraction.add (f1,f2));; 
 
 let bad = Fraction.Frac (10,20);; 
 Fraction.toString bad;;

[Output]
module type FRACTION =
  sig
    type fraction = Frac of int * int
    exception BadFrac
    val make : int * int -> fraction
    val add : fraction * fraction -> fraction
    val toString : fraction -> string
  end
module Fraction : FRACTION
val f1 : Fraction.fraction = Fraction.Frac (1, 4)
val f2 : Fraction.fraction = Fraction.Frac (1, 4)
- : string = "1/2"
val bad : Fraction.fraction = Fraction.Frac (10, 20)
- : string = "10/20"

The functions gcd and reduce are omitted from the signature, so they are invisible outside the module — clients only get make, add, and toString, and every fraction built with make is reduced. But there is a hole: because the signature reveals type fraction = Frac of int * int, a client can bypass make and build Fraction.Frac (10,20) directly — an unreduced fraction (and nothing would stop Frac (1,0) either). The invariant is broken.

The fix is the same as for stacks: make the type abstract.

OCaml REPL

module type FRACTION = sig 
    type fraction            (* hide the representation *) 
    exception BadFrac 
    val make : int * int -> fraction 
    val add : fraction * fraction -> fraction 
    val toString : fraction -> string 
 end;; 
 
 module Fraction : FRACTION = struct 
    type fraction = Frac of int * int 
    exception BadFrac 
 
    let rec gcd (x,y) = 
      let (x,y) = if x >= y then (x,y) else (y,x) in 
      if y = 0 then x else gcd (y, x mod y) 
 
    let reduce (Frac(x,y)) = 
      let d = gcd (x,y) in 
      Frac (x/d, y/d) 
 
    let make (x,y) = 
      if y = 0 then raise BadFrac 
      else reduce (Frac(x,y)) 
 
    let add (r1,r2) = 
      match (r1,r2) with 
        (Frac(a,b), Frac(c,d)) -> reduce (Frac(a*d + b*c, b*d)) 
 
    let toString (Frac(a,b)) = 
      if b = 1 then string_of_int a 
      else if a = 0 then "0" 
      else string_of_int a ^ "/" ^ string_of_int b 
 end;; 
 
 let f1 = Fraction.make (2,8);; 
 let f2 = Fraction.make (25,100);; 
 Fraction.toString (Fraction.add (f1,f2));; 
 
 Fraction.Frac (10,20);;

[Output]
module type FRACTION =
  sig
    type fraction
    exception BadFrac
    val make : int * int -> fraction
    val add : fraction * fraction -> fraction
    val toString : fraction -> string
  end
module Fraction : FRACTION
val f1 : Fraction.fraction = <abstr>
val f2 : Fraction.fraction = <abstr>
- : string = "1/2"
Line 2, characters 1-14:
2 |  Fraction.Frac (10,20);;
     ^^^^^^^^^^^^^
Error: Unbound constructor Fraction.Frac

Now the constructor Frac is unbound outside the module: the only way to create a fraction is make, which validates the denominator and reduces. Every fraction in the program is guaranteed to satisfy the invariant. This pattern — a hidden representation, a small set of smart constructors, and operations that re-establish the invariant — is one of the most important uses of the module system.

3.8 Modules and Files🔗

So far we defined modules interactively. In real projects, modules correspond to files:

  • A file foo.ml automatically defines a module named Foo (the file name, capitalized).

  • A file foo.mli, if present, is the signature for Foo: only what is declared in the .mli is visible to other files. With no .mli, everything in foo.ml is exposed.

Let us organize the abstract stack as a small project with three files. The complete code is in the course repository under notes/modules/code/.

Stack.mli — the shared signature:

module type Stack = sig
  (* The type of a stack whose elements are type 'a *)
  type 'a stack

  (* The empty stack *)
  val empty : 'a stack

  (* Whether the stack is empty *)
  val is_empty : 'a stack -> bool

  (* [push x s] is the stack [s] with [x] pushed on the top *)
  val push : 'a -> 'a stack -> 'a stack

  (* [peek s] is the top element of [s].
     Raises Failure if [s] is empty. *)
  val peek : 'a stack -> 'a

  (* [pop s] pops and discards the top element of [s].
     Raises Failure if [s] is empty. *)
  val pop : 'a stack -> 'a stack
end

liststack.ml — an implementation:

(* Stack implemented as an OCaml list *)
open Stack

module ListStack : Stack = struct
  type 'a stack = 'a list

  let empty = []
  let is_empty s = (s = [])
  let push x s = x :: s
  let peek = function
    | []   -> failwith "Empty"
    | x::_ -> x
  let pop = function
    | []    -> failwith "Empty"
    | _::xs -> xs
end

main.ml — a client program:

open Liststack

let () =
  print_endline "ListStack example";
  let s = ListStack.empty in
  let s = ListStack.push 10 s in
  let s = ListStack.push 20 s in
  let s = ListStack.push 30 s in
  Printf.printf "%d\n" (ListStack.peek s);
  let s = ListStack.pop s in
  Printf.printf "%d\n" (ListStack.peek s)

Compile each unit in dependency order, then link:

ocamlc -c Stack.mli      # produces Stack.cmi (compiled interface)
ocamlc -c liststack.ml   # produces liststack.cmo (compiled object)
ocamlc -c main.ml
ocamlc -o main liststack.cmo main.cmo
./main

Expected output:

30
20

Compiling an .mli produces a .cmi (compiled interface) file; compiling an .ml produces a .cmo (compiled object) file. To swap in the variant-based implementation, we would compile varstack.ml instead and change open Liststack to open Varstack — nothing else in main.ml changes, because the client only relies on the Stack signature. For larger projects, dune (see the previous chapter) automates this: it discovers the dependencies among files and invokes the compiler in the right order.

3.8.1 Loading Compiled Modules into the Toplevel🔗

You can also experiment with compiled modules interactively. After building with a tool that puts artifacts in _build (e.g., ocamlbuild main.byte), load them into the toplevel with the #directory and #load directives:

#directory "_build";;
#load "liststack.cmo";;
open Liststack;;

#show ListStack;;
module ListStack : Stack.Stack

# ListStack.empty;;
- : 'a Liststack.ListStack.stack = <abstr>

The #show directive prints the signature of a module — handy for exploring what a module provides (try #show List;;).

3.9 Extending a Module with include🔗

Suppose we want to add a function max to the standard List module. We cannot modify the standard library, but we can build a new module that includes it:

module MyList = struct
  include List
  let max lst = fold_left Stdlib.max (hd lst) (tl lst)
end

include List re-exports all of List’s definitions inside MyList, so #show MyList;; lists every standard list function plus our new max. Clients can use MyList as a drop-in replacement for List:

# MyList.length [1;2;3];;
- : int = 3
# MyList.max [3;7;2];;
- : int = 7

Note the difference between include and open: open List would only bring names into scope inside the definition of MyList; include List makes them part of MyList’s own interface.

3.10 Exercises🔗

Basic

  1. Define a module Circle containing a constant pi = 3.14159 and a function area : float -> float that computes the area of a circle from its radius. Use it both with the dot operator and after open.

  2. Given module A = struct let v = 1 end and module B = struct let v = 2 end, what is the value of v after open B followed by open A? Explain.

  3. Write a signature MATH that exposes only double : int -> int from a module that also defines a helper times : int -> int -> int. Verify that times is inaccessible from outside.

  4. The signature Sig declares val f : int -> int. Which of these implementations satisfy it, and why?

    let f x = x + 1
    let f x = x
    let f x = string_of_int x

Intermediate

  1. Write a signature Queue with an abstract type a queue and operations empty, enqueue, front, and dequeue. Implement it with a module ListQueue that represents a queue as a list.

  2. Give a second implementation TwoListQueue of your Queue signature that stores the queue as a pair of lists (a front list and a reversed back list). Client code should run unchanged against either implementation.

  3. Extend the abstract Fraction module with mult : fraction * fraction -> fraction and equal : fraction * fraction -> bool. Make sure mult preserves the reduced-form invariant. Why can equal be implemented with = on the representation, given the invariant?

  4. Split your Queue exercise into files (Queue.mli, listqueue.ml, main.ml) and compile it with ocamlc, then with dune.

Advanced

  1. Suppose the Stack signature made the type concrete (type a stack = a list). Give a concrete example of client code that works with ListStack but breaks when the implementation is switched to VarStack, and explain how the abstract type prevents this situation from ever arising.

  2. Design a module IntSet for sets of integers with an abstract type, maintaining the invariant that the internal list is sorted and duplicate-free. Which operations must re-establish the invariant? Could a client observe the difference if you later switched the representation to a binary search tree?

  3. Using include, build a module SafeList that extends List with safe_hd : a list -> a option and safe_tl : a list -> a list option, which return None instead of raising exceptions. Then restrict SafeList with a signature that hides the exception-raising hd and tl it inherited.

3.10.1 Project: A Store Inventory Library🔗

This is a complete, realistic project that exercises everything in this chapter: you will implement two modules, write their signatures, and build and test the result with dune and OUnit. The starter code is in the course repository under code/modules/inventory, with the following layout:

inventory/
├── dune-project
├── lib/
│   ├── dune
│   ├── item.ml            <- module Item  (you complete this)
│   └── stock.ml           <- module Stock (you complete this)
├── bin/
│   ├── dune
│   └── main.ml            <- demo executable (given)
└── test/
    ├── dune
    └── test_inventory.ml  <- OUnit test suite (given)

The lib/ directory is a dune library named inventory. Dune wraps the library’s files into one top-level module, so from the outside the modules are Inventory.Item and Inventory.Stock — modules nested inside a module. The library models a store’s inventory:

  • Item (lib/item.ml) — one product held in stock: a name, a unit price, and a quantity on hand. make must reject a negative price or quantity (an invariant, like Fraction.make). Operations: make, name, price, qty, restock, value.

  • Stock (lib/stock.ml) — a persistent collection of items with at most one item per name (another invariant). Operations: empty, is_empty, size, find, add (merges quantities when the name already exists), remove (raises Not_found for an unknown name, drops an item whose quantity reaches zero), and total_value.

The precise specifications are written as comments in the starter files and, in executable form, as the test suite. Your tasks:

  1. Implement the modules. Replace every failwith "TODO" in lib/item.ml and lib/stock.ml, checking your progress with dune build.

  2. Test with dune. Install OUnit if needed (opam install ounit2), then run dune runtest until all tests pass.

  3. Define signatures. With no .mli files, everything is exposed: clients can see that Item.t is a record and can bypass make to build an item with a negative price. Write lib/item.mli and lib/stock.mli that make both types abstract and expose only the specified operations, hiding any helpers you wrote. The tests must still pass.

  4. Check the abstraction. Temporarily add let broken = Inventory.Item.{ name = "Free monitor"; price = -129.0; qty = 4 } to bin/main.ml. It compiles before task 3 and must be rejected after. Explain why.

  5. Swap the representation. Change Stock.t to an association list (string * Item.t) list. Which files other than stock.ml did you have to touch, and why?

Build, test, and run the demo with:

dune build
dune runtest
dune exec bin/main.exe

Once the library is implemented, the demo prints:

Keyboard: 7 in stock
Total value: $825.73

The starter files, with the specifications in comments:

Starter: lib/item.ml (click to expand)

modules/inventory/lib/item.ml

(* An item held in a store's inventory.

   Representation: a record with the item's name, unit price, and
   quantity on hand. You may change this representation -- the test
   suite only uses the functions below. *)

type t = { name : string; price : float; qty : int }

(* [make name price qty] creates an item.
   Raises [Invalid_argument "Item.make"] if [price < 0.0] or [qty < 0]. *)
let make (_name : string) (_price : float) (_qty : int) : t =
  failwith "TODO"

(* Accessors. *)
let name (item : t) : string = item.name
let price (item : t) : float = item.price
let qty (item : t) : int = item.qty

(* [restock n item] is [item] with [n] more units on hand.
   Raises [Invalid_argument "Item.restock"] if [n < 0]. *)
let restock (_n : int) (_item : t) : t =
  failwith "TODO"

(* [value item] is the total value of the units on hand:
   the unit price times the quantity. *)
let value (_item : t) : float =
  failwith "TODO"

Starter: lib/stock.ml (click to expand)

modules/inventory/lib/stock.ml

(* A store's stock: a collection of items, indexed by item name.

   Representation: a list of items containing at most one item per
   name. This is an invariant your functions must maintain! You may
   change the representation -- the test suite only uses the
   functions below.

   Stocks are persistent (immutable): every operation returns a new
   stock and leaves the old one unchanged. *)

type t = Item.t list

(* The empty stock. *)
let empty : t = []

(* [is_empty s] is whether [s] contains no items. *)
let is_empty (_s : t) : bool =
  failwith "TODO"

(* [size s] is the number of distinct items in [s]. *)
let size (_s : t) : int =
  failwith "TODO"

(* [find name s] is [Some item] if [s] contains an item called
   [name], and [None] otherwise. *)
let find (_name : string) (_s : t) : Item.t option =
  failwith "TODO"

(* [add item s] adds [item] to [s]. If [s] already contains an item
   with the same name, the quantities are merged and the price of
   [item] (the most recent price) wins. *)
let add (_item : Item.t) (_s : t) : t =
  failwith "TODO"

(* [remove name n s] removes [n] units of the item called [name].
   If the quantity on hand drops to 0, the item disappears from the
   stock entirely.
   Raises [Not_found] if there is no item called [name].
   Raises [Invalid_argument "Stock.remove"] if [n < 0] or [n] is
   more than the quantity on hand. *)
let remove (_name : string) (_n : int) (_s : t) : t =
  failwith "TODO"

(* [total_value s] is the sum of the values of all items in [s]. *)
let total_value (_s : t) : float =
  failwith "TODO"

Test suite: test/test_inventory.ml (click to expand)

modules/inventory/test/test_inventory.ml

(* Test suite for the store inventory exercise.
   Run with: dune runtest *)
open OUnit2
open Inventory

let feq = cmp_float ~epsilon:1e-9

(* A small stock used by several tests:
   20 USB cables at $4.99, 10 keyboards at $29.99 *)
let sample =
  Stock.empty
  |> Stock.add (Item.make "USB cable" 4.99 20)
  |> Stock.add (Item.make "Keyboard" 29.99 10)

let item_tests =
  "Item" >::: [
    "make_and_accessors" >:: (fun _ ->
      let it = Item.make "Monitor" 129.00 4 in
      assert_equal "Monitor" (Item.name it);
      assert_equal ~cmp:feq ~printer:string_of_float 129.00 (Item.price it);
      assert_equal ~printer:string_of_int 4 (Item.qty it));

    "make_negative_price" >:: (fun _ ->
      assert_raises (Invalid_argument "Item.make")
        (fun () -> Item.make "Monitor" (-129.00) 4));

    "make_negative_qty" >:: (fun _ ->
      assert_raises (Invalid_argument "Item.make")
        (fun () -> Item.make "Monitor" 129.00 (-4)));

    "value" >:: (fun _ ->
      let it = Item.make "Monitor" 129.00 4 in
      assert_equal ~cmp:feq ~printer:string_of_float 516.00 (Item.value it));

    "restock" >:: (fun _ ->
      let it = Item.make "Monitor" 129.00 4 in
      let it = Item.restock 6 it in
      assert_equal ~printer:string_of_int 10 (Item.qty it));

    "restock_negative" >:: (fun _ ->
      let it = Item.make "Monitor" 129.00 4 in
      assert_raises (Invalid_argument "Item.restock")
        (fun () -> Item.restock (-1) it));
  ]

let stock_tests =
  "Stock" >::: [
    "empty_is_empty" >:: (fun _ ->
      assert_bool "empty stock" (Stock.is_empty Stock.empty);
      assert_equal ~printer:string_of_int 0 (Stock.size Stock.empty));

    "add_and_find" >:: (fun _ ->
      assert_equal ~printer:string_of_int 2 (Stock.size sample);
      match Stock.find "USB cable" sample with
      | None -> assert_failure "USB cable should be in stock"
      | Some it -> assert_equal ~printer:string_of_int 20 (Item.qty it));

    "find_missing" >:: (fun _ ->
      assert_equal None (Stock.find "Webcam" sample));

    "add_merges_quantities" >:: (fun _ ->
      (* Adding an item that already exists merges the quantities
         and keeps the most recent price. *)
      let s = Stock.add (Item.make "USB cable" 5.49 5) sample in
      assert_equal ~printer:string_of_int 2 (Stock.size s);
      match Stock.find "USB cable" s with
      | None -> assert_failure "USB cable should be in stock"
      | Some it ->
        assert_equal ~printer:string_of_int 25 (Item.qty it);
        assert_equal ~cmp:feq ~printer:string_of_float 5.49 (Item.price it));

    "remove_some" >:: (fun _ ->
      let s = Stock.remove "Keyboard" 3 sample in
      match Stock.find "Keyboard" s with
      | None -> assert_failure "Keyboard should still be in stock"
      | Some it -> assert_equal ~printer:string_of_int 7 (Item.qty it));

    "remove_all_drops_item" >:: (fun _ ->
      let s = Stock.remove "Keyboard" 10 sample in
      assert_equal None (Stock.find "Keyboard" s);
      assert_equal ~printer:string_of_int 1 (Stock.size s));

    "remove_missing_raises" >:: (fun _ ->
      assert_raises Not_found
        (fun () -> Stock.remove "Webcam" 1 sample));

    "remove_too_many_raises" >:: (fun _ ->
      assert_raises (Invalid_argument "Stock.remove")
        (fun () -> Stock.remove "Keyboard" 11 sample));

    "total_value" >:: (fun _ ->
      (* 20 * 4.99 + 10 * 29.99 = 99.80 + 299.90 = 399.70 *)
      assert_equal ~cmp:feq ~printer:string_of_float
        399.70 (Stock.total_value sample));

    "persistence" >:: (fun _ ->
      (* Stocks are immutable: removing from [sample] must not
         change [sample] itself. *)
      let _ = Stock.remove "Keyboard" 10 sample in
      match Stock.find "Keyboard" sample with
      | None -> assert_failure "sample must be unchanged"
      | Some it -> assert_equal ~printer:string_of_int 10 (Item.qty it));
  ]

let () =
  run_test_tt_main ("inventory" >::: [item_tests; stock_tests])

3.11 Summary🔗

Key takeaways:
  • Modules decompose large programs into units that group related types, functions, and data.

  • module M = struct ... end defines a module; members are accessed as M.x, or without the prefix after open (later opens shadow earlier ones; use local opens to limit scope).

  • module type defines a signature — an interface that lists exactly what clients may see. Anything omitted is hidden.

  • An abstract type (a type declared without a definition) hides a module’s representation. Clients can only use the operations in the signature, which lets the module enforce invariants and lets us swap implementations freely.

  • Each file foo.ml is a module Foo; a matching foo.mli is its signature. include extends an existing module with new definitions.

Terminology: module / structure (the implementation, struct ... end), signature / module type (the interface, sig ... end), signature ascription (module M : Sig), abstract type, invariant, persistent data structure, open vs. include.

Common mistakes:
  • Forgetting that module names must start with an uppercase letter.

  • Confusing open (brings names into scope) with include (re-exports names as part of the new module).

  • Exposing a type’s definition in the signature and then being surprised that clients depend on (or corrupt) the representation — hide the type if the representation is not part of the contract.

  • Expecting push/pop to mutate a stack: functional data structures return new versions and leave the old ones unchanged.

  • Declaring a name in a signature but not defining it in the structure — the module fails to type check against the signature.

Looking ahead, signatures and abstract types are OCaml’s take on ideas you will meet repeatedly in this course: interfaces and information hiding in object-oriented languages, type classes and traits (in Rust), and the general principle that a type system can enforce a design discipline. The stack and fraction modules also preview algebraic data types at work in real APIs, and the persistent-data-structure style reappears when we study interpreters and garbage collection.