3 OCaml Modules
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?
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.
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 *)
endThe 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?
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 *)
endA 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
endThe 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
endspecify a concrete type for the abstract type ’a stack, and
define all the names declared in the 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?
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 s2Pushing 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
endliststack.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
endmain.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
./mainExpected output:
30
20Compiling 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)
endinclude 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 = 7Note 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
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.
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.
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.
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
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.
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.
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?
Split your Queue exercise into files (Queue.mli, listqueue.ml, main.ml) and compile it with ocamlc, then with dune.
Advanced
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.
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?
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:
Implement the modules. Replace every failwith "TODO" in lib/item.ml and lib/stock.ml, checking your progress with dune build.
Test with dune. Install OUnit if needed (opam install ounit2), then run dune runtest until all tests pass.
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.
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.
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.exeOnce the library is implemented, the demo prints:
Keyboard: 7 in stock
Total value: $825.73The starter files, with the specifications in comments:
Starter: lib/item.ml (click to expand)
(* 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
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.
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.