🧱 Module System Explorer

Master OCaml's powerful module system — from basic modules to functors

1. Basic Modules
2. Signatures
3. Abstract Types
4. Functors
5. First-Class Modules
6. Design Patterns

What Are Modules?

Modules are OCaml's primary mechanism for organizing code. Every .ml file is implicitly a module. You can also define modules explicitly inside files.

💡 Think of modules as namespaces with superpowers — they group related types, values, and functions, but they can also be parameterized (functors) and passed as values (first-class modules).

Defining a Module

module Counter = struct type t = { mutable count : int; name : string } let create name = { count = 0; name } let increment c = c.count <- c.count + 1 let get c = c.count let reset c = c.count <- 0 let to_string c = Printf.sprintf "%s: %d" c.name c.count end

Using a Module

(* Qualified access *) let c = Counter.create "page_views" let () = Counter.increment c let n = Counter.get c (* n = 1 *) (* Open for unqualified access *) open Counter let c2 = create "clicks" (* Local open — preferred, limits scope *) let msg = Counter.(to_string (create "test")) (* or *) let msg = let open Counter in to_string (create "test")

Nested Modules

module Geometry = struct module Point = struct type t = { x : float; y : float } let origin = { x = 0.0; y = 0.0 } let distance p1 p2 = sqrt ((p1.x -. p2.x) ** 2.0 +. (p1.y -. p2.y) ** 2.0) end module Circle = struct type t = { center : Point.t; radius : float } let area c = Float.pi *. c.radius ** 2.0 end end (* Access: Geometry.Point.origin *)

🏋️ Exercise: Create a Stack Module

Define a module Stack with type t (an int list), and functions empty, push, pop, and top. pop and top should return option types.

Module Signatures (Interfaces)

A signature (or module type) describes the interface of a module — what types and values it exposes — without revealing implementation details.

💡 Signatures are to modules what .mli files are to .ml files. They define the contract.
module type PRINTABLE = sig type t val to_string : t -> string val print : t -> unit end module type COMPARABLE = sig type t val compare : t -> t -> int val equal : t -> t -> bool end

Constraining a Module

module IntPair : COMPARABLE = struct type t = int * int let compare (a1, b1) (a2, b2) = match Int.compare a1 a2 with | 0 -> Int.compare b1 b2 | c -> c let equal a b = compare a b = 0 (* This helper exists but is NOT visible outside! *) let swap (a, b) = (b, a) end (* IntPair.swap is hidden — signature hides it *)

Signature vs No Signature

AspectNo SignatureWith Signature
VisibilityEverything exposedOnly declared items visible
Type infoConcrete types visibleCan make types abstract
RefactoringAny change breaks usersInternal changes are safe
DocumentationMust read implementationSignature IS the docs

🏋️ Exercise: Write a Signature

Write a STACK signature with an abstract type t and functions empty : t, push : int -> t -> t, pop : t -> t option, top : t -> int option, and size : t -> int.

Abstract Types & Encapsulation

Abstract types are the key to encapsulation in OCaml. When a signature declares type t without = ..., the internal representation is hidden from users.

module type SET = sig type elt (* element type — abstract *) type t (* set type — abstract *) val empty : t val add : elt -> t -> t val mem : elt -> t -> bool val size : t -> int val to_list : t -> elt list end (* Implementation using a sorted list *) module IntSet : SET with type elt = int = struct type elt = int type t = int list (* hidden! users can't see it's a list *) let empty = [] let rec add x = function | [] -> [x] | h :: _ as l when x < h -> x :: l | h :: _ as l when x = h -> l | h :: t -> h :: add x t let rec mem x = function | [] -> false | h :: _ when x = h -> true | h :: _ when x < h -> false | _ :: t -> mem x t let size = List.length let to_list l = l end
💡 The with type elt = int sharing constraint reveals just the element type so users can create sets of ints, while keeping the internal representation (t = int list) hidden.

Why Abstract Types Matter

┌─────────────── Public API ───────────────┐ │ type elt = int │ │ type t = ??? (abstract — can't peek!) │ │ val empty : t │ │ val add : int -> t -> t │ │ val mem : int -> t -> bool │ └──────────────────────────────────────────┘ ▲ Users see this ┌─────────── Implementation ───────────────┐ │ type t = int list (sorted) │ │ ← Could change to balanced BST later! │ │ ← No user code breaks. │ └──────────────────────────────────────────┘ ▲ Only module author sees this

🏋️ Exercise: Spot the Leak

This module has an abstraction leak. What is it, and how would you fix it?

module type COUNTER = sig type t = int (* ← is this right? *) val zero : t val incr : t -> t val value : t -> int end

Functors — Parameterized Modules

A functor is a function from modules to modules. It takes a module (matching a signature) as input and produces a new module as output. This is OCaml's most powerful abstraction tool.

💡 Functors let you write code that's generic over module-level abstractions. Think: "Give me any module with a compare function, and I'll give you back a complete Set implementation."
(* Input signature: what the functor needs *) module type ORDERED = sig type t val compare : t -> t -> int end (* Functor: produces a Set for any ordered type *) module MakeSet (Ord : ORDERED) = struct type elt = Ord.t type t = elt list let empty = [] let rec add x = function | [] -> [x] | h :: t -> match Ord.compare x h with | c when c < 0 -> x :: h :: t | 0 -> h :: t (* duplicate *) | _ -> h :: add x t let rec mem x = function | [] -> false | h :: t -> match Ord.compare x h with | 0 -> true | c when c < 0 -> false | _ -> mem x t let to_list s = s let size = List.length end (* Using the functor *) module StringSet = MakeSet(struct type t = string let compare = String.compare end) module FloatSet = MakeSet(struct type t = float let compare = Float.compare end) let s = StringSet.(add "hello" (add "world" empty)) let has = StringSet.mem "hello" s (* true *)

Functor Anatomy

module MakeSet (Ord : ORDERED) = struct ... end ─────── ─────── ─── ─────── ───────────── │ │ │ │ │ │ functor │ signature output module │ name │ constraint (the body) │ │ keyword parameter name

Real-World: Map from Stdlib

(* OCaml's standard Map uses exactly this pattern *) module StringMap = Map.Make(String) module IntMap = Map.Make(Int) let m = StringMap.empty |> StringMap.add "name" "OCaml" |> StringMap.add "version" "5.1" let name = StringMap.find "name" m (* "OCaml" *)

🏋️ Exercise: Write a Functor

Write a MakePriorityQueue functor that takes an ORDERED module and provides empty, insert, and pop_min (returns (elt * t) option). Use a sorted list internally.

First-Class Modules

OCaml lets you pack modules into values and unpack them back. This bridges the module system and the core language, enabling powerful dynamic patterns.

module type SHOW = sig type t val show : t -> string end (* Pack a module into a value *) let int_shower : (module SHOW with type t = int) = (module struct type t = int let show = string_of_int end) (* Unpack and use *) let show_value (type a) (module S : SHOW with type t = a) (x : a) = S.show x let result = show_value (module struct type t = float let show = Float.to_string end) 3.14 (* result = "3.14" *)

Heterogeneous Collections

(* Store different "showable" things in one list *) type showable = Showable : (module SHOW with type t = 'a) * 'a -> showable let items = [ Showable ((module struct type t = int let show = string_of_int end), 42); Showable ((module struct type t = string let show x = x end), "hi"); Showable ((module struct type t = bool let show = Bool.to_string end), true); ] let show_all items = List.map (fun (Showable ((module S), v)) -> S.show v) items (* ["42"; "hi"; "true"] *)
💡 First-class modules are essential for plugin systems, dynamic dispatch, and when you need to choose implementations at runtime rather than compile time.

🏋️ Exercise: Module Selection

Write a function pick_serializer that takes a format string ("json" or "csv") and returns the appropriate first-class module matching a SERIALIZER signature with val serialize : string list -> string.

Module Design Patterns

1. The "Make" Pattern

Standard library convention: a functor named Make that produces a full implementation from a minimal input.

(* Used everywhere in stdlib *) module StringSet = Set.Make(String) module IntMap = Map.Make(Int) module MyHashtbl = Hashtbl.Make(struct type t = string * int let equal (a1,b1) (a2,b2) = a1 = a2 && b1 = b2 let hash (a,b) = Hashtbl.hash a lxor Hashtbl.hash b end)

2. The "T" Convention

Name your main type t so it reads naturally with the module name.

(* User.t, Token.t, Config.t — reads like English *) module User = struct type t = { id : int; name : string; email : string } let create ~id ~name ~email = { id; name; email } let to_string u = Printf.sprintf "%s <%s>" u.name u.email end

3. Include for Extension

Use include to build larger modules from smaller ones.

module type BASE = sig type t val compare : t -> t -> int end module type EXTENDED = sig include BASE val equal : t -> t -> bool val min : t -> t -> t val max : t -> t -> t end (* Implement EXTENDED from any BASE *) module Extend (B : BASE) : EXTENDED with type t = B.t = struct include B let equal a b = compare a b = 0 let min a b = if compare a b <= 0 then a else b let max a b = if compare a b >= 0 then a else b end

4. Phantom Types via Modules

module Id : sig type _ t (* phantom type parameter *) type user type post val user_id : int -> user t val post_id : int -> post t val to_int : _ t -> int end = struct type _ t = int type user type post let user_id n = n let post_id n = n let to_int n = n end (* Now the type system prevents mixing IDs! *) let uid = Id.user_id 42 let pid = Id.post_id 42 (* These are different types — can't mix them *)

Quick Reference

PatternWhen to UseExample
module M = struct ... endSimple namespaceGroup related functions
module type S = sig ... endDefine an interfaceContract for plugins
M : SHide internalsAbstract data types
module F(X:S) = struct...endGeneric over typesSet.Make, Map.Make
(module M : S)Runtime dispatchPlugin selection
include MExtend a moduleAdd helpers to Base
S with type t = ...Reveal one typeShare element type