Decoupling Logic with OCaml Functors
Learn how OCaml Functors allow you to build reusable, type-safe components by treating modules as first-class arguments to create specialized implementations at compile-time.
19 Jun 2026, 18:02 UTC

The Problem: Hard-Coded Dependencies
When building a system that needs to handle different types of data—such as a cache that could store either integers or strings—developers often reach for generics (parametric polymorphism). However, generics only solve the problem of type. They don't solve the problem of behavior.
If your cache needs to know how to compare two elements to determine if they are the same key, a generic 'a isn't enough. You need a specific comparison function. In many languages, this leads to passing comparison functions as arguments to every single call or relying on runtime interfaces (dynamic dispatch), which can introduce performance overhead and hide type errors until the program is actually running.
OCaml solves this using Functors. A functor is essentially a function that takes a module as an input and returns a new module as an output. This allows you to define the logic of a component once and instantiate it with different concrete implementations at compile-time.
Defining the Interface with Signatures
Before creating a functor, you must define a Signature. A signature acts as a contract; it tells the functor exactly what the input module must provide. This is the OCaml equivalent of an interface in Java or a trait in Rust, but it operates at the module level.
For a generic Set or Cache, the signature would require a type and a way to compare that type. By defining this interface, you ensure that any module passed to your functor is guaranteed to have the necessary operations, preventing runtime crashes.
Practical Example: A Generic Set Functor
In this example, we create a MakeSet functor. It takes a module that knows how to compare elements and returns a module that provides set operations.
(* The signature: What the input module MUST provide *)
module type COMPARABLE = sig
type t
val compare : t -> t -> int
end
(* The Functor: Logic that works for ANY module matching COMPARABLE *)
module MakeSet (Element : COMPARABLE) = struct
type t = Element.t list
let add x s =
if List.exists (fun y -> Element.compare x y = 0) s then s
else x :: s
let contains x s =
List.exists (fun y -> Element.compare x y = 0) s
end
(* Concrete implementation 1: Integers *)
module IntComparable = struct
type t = int
let compare = Int.compare
end
(* Concrete implementation 2: Strings *)
module StringComparable = struct
type t = string
let compare = String.compare
end
(* Instantiating the functor to create specific modules *)
module IntSet = MakeSet(IntComparable)
module StringSet = MakeSet(StringComparable)
How to verify the result
You can verify this in the OCaml REPL (utop). Try adding an integer to an IntSet; it will work. If you try to add a string to an IntSet, the compiler will throw a type error immediately. This proves that the functor has created two distinct, type-safe modules from a single piece of logic.
Trade-offs and Limitations
Functors are powerful, but they are not a silver bullet. The primary trade-off is cognitive overhead. For developers coming from object-oriented backgrounds, the distinction between a module and a functor can be confusing. The syntax for nested modules can also become verbose, making type signatures difficult to read when functors are nested several layers deep.
Additionally, while functors avoid the runtime cost of dynamic dispatch, deeply nested functor applications can increase compilation times in very large projects, as the compiler must instantiate and type-check each specific module generated.
When to Choose Functors Over Generics
Use standard parametric polymorphism (generics) when the logic is identical regardless of the type (e.g., reversing a list). Use functors when the logic depends on behavior defined by the type (e.g., sorting, hashing, or database connectivity).
By shifting dependency injection from runtime to compile-time, you create a codebase that is both highly flexible and mathematically sound.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.