Cutting Boilerplate with ppx_deriving: Where OCaml's Code Generation Pays Off
ppx_deriving generates compare, show, and equality functions from one annotation, keeping them correct as types evolve — but generated code has real costs in error readability and build complexity worth understanding first.
20 Mar 2026, 09:41 UTC

Every OCaml project eventually accumulates the same tedious trio: a compare function so your type works in Set and Map, a show function so you can debug, and maybe a serializer. Writing these by hand is mechanical, and worse, they silently rot when someone adds a constructor and forgets to update them. ppx_deriving solves this by generating the functions at compile time from a single annotation on the type declaration.
The thesis here is simple: deriving is worth adopting for structural boilerplate, but you should be deliberate about which derivers you use and where, because generated code has real costs in readability of errors and in build complexity.
What the annotation actually does
PPX rewriters are compile-time transformations over the typed AST — they run after parsing, before type checking of the generated code, so the output is ordinary OCaml that the compiler checks like anything you wrote. With ppx_deriving installed, you write:
type t =
| Leaf of int
| Node of t * t
[@@deriving show, eq, ord]This generates show : t -> string, equal : t -> t -> bool, and compare : t -> t -> int, all recursive over your structure. The ord deriver is the one that earns its keep most often: it makes the type usable with Set.Make and Map.Make functors without you writing a comparison by hand. Because the generated code is plain OCaml, it inlines and optimizes like handwritten code — there is no runtime reflection layer and no meaningful performance penalty for typical workloads.
A worked example: a type that stays correct under edits
Consider a small configuration record:
type endpoint = {
host : string;
port : int;
tls : bool;
} [@@deriving show, eq, ord]
module EndpointSet = Set.Make (struct
type nonrec t = endpoint
let compare = compare_endpoint
end)Note the type nonrec t = endpoint trick — the functor argument needs a type named t, and nonrec lets it refer to the outer endpoint rather than itself. Now when a teammate adds a priority : int field, the derived compare and show update automatically. A handwritten compare would have compiled fine while silently ignoring the new field, producing a set that treats distinct endpoints as duplicates. That class of bug is exactly what deriving eliminates.
To try this, install with opam install ppx_deriving and add (preprocess (pps ppx_deriving.show ppx_deriving.eq ppx_deriving.ord)) to your dune file's library or executable stanza. Check your installed version with ocamlfind query -format %v ppx_deriving — deriver names and availability have shifted across releases, so confirm against the changelog for your version rather than assuming an old tutorial still applies.
Inspecting what got generated
Because the generated code is invisible in your source, debugging surprises means looking at it. Dune can show you the preprocessed output:
dune describe pp path/to/module.mlRun this from your project root; it prints the module after all PPX rewrites. This is the single most useful habit when a type error points at a line that doesn't exist in your file — the error is real, it's just in generated code. Verifying a new deriver this way takes a minute and beats guessing.
The trade-offs, honestly stated
Three costs deserve attention. First, error messages: when a derived function fails to typecheck — say you derive ord on a type containing a function value — the compiler reports the error inside code you never wrote, which confuses newcomers and occasionally veterans. Second, PPX stacking: combining several rewriters (deriving plus a syntax extension like a monadic let-syntax) can produce ordering issues or conflicts, and each additional PPX slows compilation since each one may re-parse or re-traverse the AST. Third, ecosystem drift: some derivers have been deprecated or folded into other packages over time, so a pinned opam switch and a quick compile check after upgrades are worthwhile.
The practical rule many projects converge on: derive freely for show, eq, and ord on plain data types; be cautious deriving serializers on types whose shape is a public contract, because a silent field rename becomes a silent wire-format change. For those, explicit versioning or hand-reviewed codecs are safer.
Where to start
Pick one module with a handwritten compare or to_string, replace it with [@@deriving show, ord], run your test suite, and inspect the result with dune describe pp. If the tests pass and the generated code looks like what you deleted, you have your answer. Keep handwritten implementations only where the logic is genuinely non-structural — that's the line where deriving stops helping and starts hiding.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.