Overview
Large software systems rely on modular design — dividing programs into components that can be developed, tested, and compiled independently.
Programming languages formalize this through modules, signatures, and separate compilation.
Together, these mechanisms enable:
- Encapsulation — hiding implementation details.
- Abstraction — exposing only conceptual interfaces.
- Reusability — compiling and linking components without re-analyzing the whole system.
- Safety — type checking across module boundaries.
Note
The module system generalizes the idea of scope from single files to entire components.
Where functions provide control abstraction, modules provide data and structural abstraction.
Modules
A module is a named collection of related definitions — values, types, functions, and submodules.
It is both a namespace and a compilation unit.
Example (OCaml-style)
module Stack = struct
type t = int list
let empty = []
let push x s = x :: s
let pop = function
| [] -> failwith "Empty stack"
| x :: xs -> (x, xs)
end-
Stack.tdefines the abstract type of stacks. -
Functions
push,pop, andemptybelong to the module’s namespace. -
Access via
Stack.push,Stack.pop, etc.
Tip
Modules can group any collection of types and functions — not just ADTs.
They act as logical boundaries for cohesion and reuse.
Signatures — The Interface Layer
A signature specifies what a module exposes: the types and values that form its public contract.
It hides private details, ensuring clients depend only on the interface, not the implementation.
Example
module type STACK = sig
type t
val empty : t
val push : int -> t -> t
val pop : t -> int * t
endKey Points
-
Abstract types:
type thides its concrete definition. -
Exposed operations: define how users interact with the abstract type.
-
Interface stability: as long as the signature stays the same, implementation can change freely.
Matching Implementation to Signature
module Stack : STACK = struct
type t = int list
let empty = []
let push x s = x :: s
let pop s = (List.hd s, List.tl s)
endHere, the compiler ensures the implementation matches the signature — every declared value and type must be defined with the correct type.
Note
In ML-family languages, signature matching is statically verified, not at runtime.
This is essential for separate compilation.
Abstraction and Encapsulation
Signatures create abstraction barriers: the user knows how to use a module but not how it works internally.
-
Internal types, helper functions, and optimizations remain private.
-
The compiler enforces the interface boundary.
Example:
module Counter : sig
type t
val new : unit -> t
val inc : t -> unit
val get : t -> int
end = struct
type t = int ref
let new () = ref 0
let inc c = c := !c + 1
let get c = !c
endHere, users cannot access the underlying int ref.
They interact only through new, inc, and get.
Warning
Exposing concrete types accidentally (e.g.,
type t = int listinstead oftype t) breaks encapsulation and ties clients to internal design choices.
Functors — Parameterized Modules
Modules can be parameterized by other modules — analogous to functions operating on modules instead of values.
Example
module type STACK = sig
type t
val empty : t
val push : int -> t -> t
val pop : t -> int * t
end
module StackArray () : STACK = struct
type t = int list
let empty = []
let push x s = x :: s
let pop s = (List.hd s, List.tl s)
endOr, more generally:
functor MakeStack (Element : sig type t end) : sig
type t
val empty : t
val push : Element.t -> t -> t
val pop : t -> Element.t * t
end = struct
type t = Element.t list
let empty = []
let push x s = x :: s
let pop s = (List.hd s, List.tl s)
endTip
Functors allow generic programming without inheritance — each instantiation produces a specialized module.
Separate Compilation
With modules and signatures, compilers can check and compile each module independently:
-
Parse and type-check against its signature.
-
Generate compiled code (
.cmo,.o, or equivalent). -
Link precompiled modules later.
Example Flow
stack.ml
stack.mli
main.ml
Steps:
-
Compile interface:
ocamlc -c stack.mli→stack.cmi -
Compile implementation:
ocamlc -c stack.ml -
Compile main:
ocamlc -c main.ml -
Link:
ocamlc -o app stack.cmo main.cmo
Note
The
.mlifile (signature) is compiled first — guaranteeing other modules can compile against it without needing implementation details.
Benefits of Separate Compilation
| Benefit | Description |
|---|---|
| Scalability | Large projects compile faster — only changed modules rebuild. |
| Safety | Type-checked module boundaries prevent interface mismatches. |
| Encapsulation | Private data remains hidden, protecting invariants. |
| Reusability | Common libraries are linked, not recompiled. |
Tip
Separate compilation is foundational for modular systems like Standard ML, OCaml, Ada, and Modula-3, and conceptually underpins package systems in languages like Rust and Go.
Common Pitfalls
Warning
Leaky abstraction: exposing concrete types or constructors in a public signature.
Circular dependencies: modules referring to each other without explicit functorization.
Incomplete signatures: missing declarations prevent consistent type-checking.
Name collisions: improper namespace scoping leads to ambiguous symbols.
Diagram Concepts
-
module_interface_boundary.svg:
Two boxes — Implementation and Signature — with arrows showing exported identifiers and hidden internals.
Highlight that clients depend only on the interface layer. -
separate_compilation_pipeline.svg:
Modules A, B, and Main each compile independently to object code.
Final link step combines compiled artifacts using consistent type signatures.