Reading — step 1 of 7
Learn
OCaml's modules are usually compile-time constructs. First-class modules lift them to values — pass them around, store them, return them from functions. The OCaml manual treats this as a power feature for plugin systems and dependency injection.
Module types and modules
module type Comparable = sig
type t
val compare : t -> t -> int
end
module IntCmp : Comparable with type t = int = struct
type t = int
let compare a b = compare a b
end
module StringCmp : Comparable with type t = string = struct
type t = string
let compare a b = String.compare a b
end
Normally these are compile-time types. They can also be used as VALUES.
First-class module syntax
Use (module M) to package a module as a value:
let m1 : (module Comparable with type t = int) = (module IntCmp)
let m2 : (module Comparable with type t = string) = (module StringCmp)
The parens around module M are crucial. The type (module SIG) is the type of first-class module values.
Unpacking — using a first-class module
let sort (type a) (m : (module Comparable with type t = a)) (xs : a list) =
let module M = (val m) in
List.sort M.compare xs
(type a)— locally abstract type at the function head(val m)— extracts the module from the valuelet module M = ... in ...— binds it locally
Now sort (module IntCmp) [3; 1; 2] works.
Use case: dependency injection
module type Logger = sig
val log : string -> unit
end
module ConsoleLogger : Logger = struct
let log msg = print_endline ("LOG: " ^ msg)
end
module SilentLogger : Logger = struct
let log _ = ()
end
let do_work (logger : (module Logger)) =
let module L = (val logger) in
L.log "starting";
L.log "finishing"
let () =
do_work (module ConsoleLogger);
do_work (module SilentLogger)
Different logger implementations selected at runtime. Common pattern for testing (silent logger in tests, real one in prod).
When to use first-class modules
- Plugin systems — load modules from config
- Dependency injection — swap implementations for tests
- Strategy pattern — pick algorithm at runtime
- Heterogeneous collections — list of modules implementing the same signature
vs Functors
Functors (covered earlier) are COMPILE-TIME parameterized modules. First-class modules are RUNTIME values.
(* Functor: compile-time *)
module SortByCompare (M : Comparable) = struct
let sort xs = List.sort M.compare xs
end
(* First-class: runtime *)
let sort (m : (module Comparable with type t = _)) xs = ...
Functors give you stronger compile-time guarantees and zero runtime cost. First-class modules let you select dynamically. Pick based on whether the choice is known at compile or run time.
Type sharing
The with type t = INT constraint exposes the underlying type:
(module Comparable with type t = int) (* type t exposed as int *)
(module Comparable) (* type t is abstract *)
Without the with type constraint, you can't pass values to compare — the type is abstract.
Common mistakes
- Forgetting
(module ...)parens — required for the first-class form. - Forgetting
(val m)to unpack — using m directly as a module gives a type error. - Type sharing — without
with type t = ACTUAL, the abstract type is unusable. - Reaching for first-class modules when functors suffice — compile-time is stronger when applicable.
(type a)annotation forgotten — needed when the function is generic in the module's type.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…