A structure is simply a collection of definitions with name.

1
2
3
4
5
module MyModule = struct
let inc x = x + 1
type primary_color = Red | Green | Blue
exception Oops
end

The output has form module MyModule: sig ... end

Formally, the syntax is

1
2
3
4
5
6
module ModuleName = module_expression

(* where one way of writing module_expression is [struct end] *)
module ModuleName = struct
module_items
end

Scope and Open

Once we have defined a module, we can access the members with . notation.

1
2
3
4
5
6
7
8
9
module M = struct
let x = 1
end

(* This is valid *)
M.x

(* This is not valid *)
x

To make the second valid, we may “open a module”, similar to using namespace xxx; in C++, to bring definitions to outer space.

We can also open a module in module expression.

1
2
3
4
5
module M = struct
open List

(* This map here is List.map *)
let uppercase_all = map String.uppercase_ascii

Thus, we may want to limit the scope of opening, for example, we may use Module.(e) grammar, which means that, all names used in e is from Module. Or the let open Module in e expression.

1
2
3
4
5
6
7
8
(* Long *)
let s' = s |> String.trim |> String.lowercase_ascii
(* equiv short *)
let s'' = String.(s |> trim |> lowercase_ascii)

let lowertrim s =
let open String in
s |> trim |> lowercase_ascii

Module Type Definition

OCaml will infer a signature as the type of a module, but we can also hand-write. For example,

1
2
3
4
5
6
7
8
module type LIST_STACK = sig
exception Empty
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

We then define the module, and tell OCaml that ListStack have the module type specified by LIST_STACK.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module ListStack : LIST_STACK = struct
let empty = []

let is_empty = function [] -> true | _ -> false

let push x s = x :: s

exception Empty

let peek = function
| [] -> raise Empty
| x :: _ -> x

let pop = function
| [] -> raise Empty
| _ :: s -> s
end

Note that, the type must exactly fit. Otherwise, OCaml will report an error.

Semantics of Module Types

What does it mean when we write the :T in module M : T = ...? These are 2 properties that the compiler guarantees:

  1. Signature matching.
  2. Encapsulation. Any name in M but not in T is not visible to code outside of M.

Another important factor involves subtyping. For example, in Java, we may code like

1
2
3
4
5
class C {}
class D extends C {}

D d = new D();
C c = d;

Subtyping is what permits the assignment of c <- d.

I thinks it’s like OCaml’s way of inheritence (as in C++, Java) or composition (as in Rust).

Suppose module type X = sig val x : int end, module type Z = sig val z : int end and module M = struct let x = 1;; let z = 2 end. It might be surprising to see module MXZ = ((M : X) : Z) is valid. But indeed, this notation implies an inheritence between X, Z.

Loading Compiled Modules

Suppose a file called mods.ml, and we have some code in it.

mods.ml
1
2
3
4
5
6
let b = "bigred"
let inc x = x + 1

module M = struct
let y = 42
end

Compiling with ocamlc mods.ml will generate a mods.cmo, a compiled module object file, which we can import in REPL with #load "mods.cmo";;. The module compiled is Mods, which is

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module Mods = struct
let b = "bigred"
let inc x = x + 1

module M = struct
let y = 42
end
end

(* These calls will be successful *)
Mods.b
Mods.M.y

(* This call will fail *)
inc (* correct should be Mods.inc *)

Initializing at Top Level

We can use open instead of load to directly import names from module into toplevel, so we don’t need to Mods. every time.

1
2
3
4
5
open Mods;;

(* this becomes valid now *)
inc;;
M.y;;

Requiring Libraries

To open OUnit2, we have to first load it first with require.

1
2
3
4
#require "ounit2";;

(* Then open the module *)
open OUnit2;;

Encapsulation, Visibility with Modules

Visibility

1
2
3
4
5
6
7
8
9
10
module type C_PUBLIC = sig
val y : int
end

module C_PRIVATE = struct
val x : int
val y : int
end

module C : C_PUBLIC = C_PRIVATE

Thus, we expose only y and hides x in module C.