Modules

Fun and Profit with OCaml

Modules and Abstraction

Part 3 of 3

Structures

Defining structures

All OCaml programs are organised into modules. The simplest form of module is a structure. You can think of structures as collections of definitions. Structures can be created using the module and struct keywords:

Defining structures

module M = struct type t = T let x = T end

Accessing structure components

The components of a module can be accessed using the . operator:

Accessing structure components

let y = M.x
let y = M.x

Note that the . operator works for types as well as values: the y variable defined above has type M.t.

Files as structures

In OCaml every source file defines a structure. For example, a file called foo.ml would be treated as the definition of a module called Foo. We have already used such modules in earlier examples: for instance the List.map function of the standard library is defined in a file called list.ml.

Files as structures

Signatures and abstraction

Just as all values in OCaml have a type, all modules have a module type. As you can see from the output, the module M defined above has the module type:

Signatures and abstraction

sig
  type t = T
  val x : t
end
sig
  type t = T
  val x : t
end

This means that it contains a variant type t with a single T constructor, and a value x of type t. The module types of structures, like the one above, are often called signatures.

Signature ascription

While OCaml will infer the module type of a structure from its definition, you can also ascribe it a more restricted signature. This allows us to hide some of the details of the structure:

A named signature

module type INT_SET = sig type t val empty : t val mem : int -> t -> bool val add : int -> t -> t end

Signature ascription

module IntSet : INT_SET = struct type t = int list let empty = [] let mem i s = let is_i j = (i = j) in List.exists is_i s let add i s = if mem i s then s else i :: s end

What ascription hides

Why does restricting IntSet's signature to omit the definition of t matter?

  • It makes IntSet.add run faster.
  • It hides the implementation detail that t = int list, so client code cannot depend on it, and the implementation can later change (say, to a tree) without breaking callers.
  • It's required syntax; OCaml refuses to compile a module signature without hiding at least one type.
  • It prevents IntSet from ever being used as a functor argument.

Why: this is abstraction: as long as callers only use the names published in the signature (empty, mem, add, and the opaque type t), the module's author is free to change how t is represented internally. Nothing outside the module can type-check code that assumes t is secretly a list.

Here we create an IntSet module with a type t representing sets of integers.

let s = IntSet.add 6 (IntSet.add 5 IntSet.empty) let b = IntSet.mem 6 s

By not including the definition of t in the signature, we hide the implementation of IntSet. This means that users of our set type cannot depend on the fact we have implemented it using lists.

Abstraction, enforced

let r = 4 :: s
Line 1, characters 13-14:
Error: This expression has type IntSet.t
       but an expression was expected of type int list
let r = 4 :: s
Line 1, characters 13-14:
Error: This expression has type IntSet.t
       but an expression was expected of type int list

Live demo: change the representation

The useful part of abstraction is not the error by itself; it is what the error makes possible. In the live session:

  1. Run the IntSet implementation and its client.
  2. Show that 4 :: s is rejected.
  3. Replace only the implementation with the version below, then rerun the unchanged client.

Live demo: swap the implementation

Keep INT_SET and the client unchanged. Replace only IntSet:

module IntSet : INT_SET = struct type t = Set of int list let empty = Set [] let mem i (Set xs) = List.mem i xs let add i (Set xs as s) = if List.mem i xs then s else Set (i :: xs) end

Then rerun:

let s = IntSet.add 6 (IntSet.add 5 IntSet.empty) let b = IntSet.mem 6 s

The client still works because it depended on INT_SET, not on the hidden list representation.

Types with hidden definitions, like t above, are called abstract types. OCaml's support for abstraction is one of its most important and powerful features.

Signatures for files

To add a signature to the module represented by a file we add an interface file. For example, if a file called foo.ml defines a structure called Foo then foo.mli defines the signature of Foo. Corresponding to the list.ml in the OCaml standard library, we have list.mli which describes the signature of the list interface.

Signatures for files