When Ordinary Variants Get in the Way: Reaching for OCaml's Polymorphic Variants
Ordinary OCaml variants force duplication or wrapper types when modules share overlapping sets of cases. Polymorphic variants solve this — here's a worked config-parser example, plus the exhaustiveness trade-off you need to manage.
02 May 2026, 20:21 UTC

You're writing a small configuration parser in OCaml. The file format has a handful of value shapes — strings, integers, booleans, lists, nested tables — and the natural encoding is a variant type. Then a second module needs almost the same shapes, plus two more. You either duplicate constructors, wrap one variant inside another, or merge everything into one bloated type that every consumer has to pattern-match exhaustively. None of these options feel good.
The takeaway: OCaml's polymorphic variants let you share and extend sets of tags across modules without wrappers or duplication, while keeping type checking and (with closed types) exhaustiveness checking. They are a real engineering tool, not just a curiosity — but they trade away some inference precision, so they belong in specific places.
The pain point with ordinary variants
Suppose a JSON-like configuration value:
type value =
| Str of string
| Num of float
| Bool of bool
| List of value list
| Table of (string * value) listNow a second part of the program — say, a template engine — wants the same scalar values but adds Raw of bytes. With ordinary variants you cannot extend value; you must define a new type with a Base of value wrapper, and every pattern match gains a layer of unwrapping. Constructor names are also global to their type, so two types can't both define Num without one shadowing the other in scope.
The same data with polymorphic variants
Polymorphic variants write the tag inline, prefixed with a backtick, and the type is a set of tags rather than a named declaration:
let parse_scalar = function
| "true" -> `Bool true
| "false" -> `Bool false
| s -> (try `Num (float_of_string s) with _ -> `Str s)The inferred type is something like [> `Bool of bool | `Num of float | `Str of string ] — an open type meaning "at least these tags." Any function that matches on a subset of those tags accepts this value without conversion:
let to_string = function
| `Str s -> s
| `Num f -> string_of_float f
| `Bool b -> string_of_bool b
| `List _ | `Table _ -> "<compound>"
| `Raw _ -> "<raw>"The template engine can construct `Raw bytes and pass it to shared helpers that only care about the scalar tags. No wrapper, no duplication, and the compiler still rejects a tag you never handled if the type is closed.
Worked example: parsing then narrowing to a record
A practical pattern is to parse into a polymorphic-variant tree, then narrow to a plain record at the boundary:
type config = { host : string; port : int; debug : bool }
let to_config fields =
let find k = List.assoc_opt k fields in
match find "host", find "port", find "debug" with
| Some (`Str h), Some (`Num p), Some (`Bool d) ->
Ok { host = h; port = int_of_float p; debug = d }
| _ -> Error "missing or mistyped field"Run this through ocamlc example.ml (OCaml ≥ 4.02; syntax settled by 4.04) or paste it into utop to inspect the inferred types. The win is that intermediate code — walkers, validators, serializers — can be written once against whichever subset of tags it understands, instead of once per concrete variant type.
The trade-off: open types and weaker inference
The catch is real. With open variant types ([> ...]), the compiler cannot warn you about an unhandled tag, because the type explicitly says "more tags may exist." Exhaustiveness checking only returns when you close the type, either with a lower bound ([< `A | `B ]) or a type abbreviation naming an exact set:
type scalar = [ `Str of string | `Num of float | `Bool of bool ]Annotating function parameters with such closed abbreviations restores exhaustiveness warnings and also produces much better error messages. Without annotations, polymorphic-variant type errors are notoriously cryptic — long inferred tag sets printed in full — which is the main reason teams hesitate to adopt them. They also don't work with some features that rely on nominal type identity, and inferred types can drift unexpectedly across module boundaries if you leave everything unannotated.
When to reach for them
A practical checklist:
- Use them at API boundaries and in DSLs where multiple consumers share overlapping tag sets.
- Always publish closed type abbreviations for the sets you intend to be exhaustive, and annotate public function signatures.
- Avoid them for internal, stable enumerations — an ordinary variant is simpler, gives better errors, and is easier to refactor.
- Verify behavior against the OCaml manual's polymorphic variants chapter for your compiler version.
Used this way, polymorphic variants remove a genuine class of boilerplate without giving up the type safety that made you choose OCaml in the first place.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.