Principal Types and Parametricity

Two of the deepest facts about Hindley–Milner polymorphism close this module. The first looks inward, at the inference algorithm: whatever type W hands back is not merely a type but the type — the single most general one, the principal type, from which every other valid typing is carved by substitution. The second looks outward, at the meaning of the type itself: a polymorphic type is so constraining that it dictates, for free, powerful theorems about every function that could ever have it — Reynolds' and Wadler's parametricity, the source of "theorems for free". One says inference is optimal; the other says the types you infer are astonishingly informative. Together they explain why HM types feel less like bookkeeping and more like specifications.

The principal-type property

Order types by generality. Say scheme \sigma_1 is more general than \sigma_2, written \sigma_1 \sqsubseteq \sigma_2, when \sigma_2 can be obtained from \sigma_1 by instantiating its quantified variables. Thus \forall\alpha.\,\alpha\to\alpha is more general than \mathsf{Int}\to\mathsf{Int}, which sits below it as a special case.

The reveal shows the lattice for the identity function: the principal \forall\alpha.\,\alpha\to\alpha at the top, with each concrete instance hanging beneath it as a substitution. Every legal type for id is somewhere in this cone; W finds the apex.

Principality is what makes separate compilation and clean module interfaces possible: a function can be given its principal type once, published, and every caller instantiates that one scheme to their needs. Without a most general type there would be no canonical signature to expose — you would have to guess how specifically to commit, and guess wrong.

What a polymorphic type forbids

Here is the pivot from inference to meaning. A parametric function, we said, cannot inspect the type it is handed. That negative — "it may not look" — has an astonishingly positive consequence: the type alone tells you almost everything the function can do. Consider the humblest signature:

f : \forall\alpha.\,\alpha \to \alpha.

What functions can have this type? The caller supplies \alpha and a value of type \alpha; f must return a value of type \alpha. But f knows nothing about \alpha — it cannot conjure an \alpha from thin air, cannot test it, cannot transform it, because it has no operation that works at an arbitrary type. The only value of type \alpha it can possibly return is the one it was given. So (ignoring nontermination and effects) the identity is the unique inhabitant of \forall\alpha.\,\alpha\to\alpha. The type is the specification. Compare \forall\alpha.\,\alpha\to\alpha\to\alpha: a function must return one of its two arguments, and by parametricity it can only ever return the first or (consistently) the second — exactly the two Booleans. The type pins the behaviour down to a finite menu.

Polymorphic typeWhat parametricity forces
\forall\alpha.\,\alpha\to\alphamust be the identity
\forall\alpha.\,\alpha\to\alpha\to\alphareturns 1st or 2nd arg (the two "Booleans")
\forall\alpha.\,\mathsf{List}\,\alpha\to\mathsf{List}\,\alphaoutput is a rearrangement/selection of the input's elements — no new elements can appear
\forall\alpha.\,\alpha\to\mathsf{Int}ignores its argument; returns a fixed integer

Parametricity: theorems for free

Reynolds (1983) made this precise with his abstraction theorem, and Wadler (1989) popularised its consequences under the irresistible title "Theorems for Free!". The core idea is relational: interpret each type not as a set of values but as a relation between two interpretations, and read a universally quantified type as "related inputs go to related outputs, for every choice of relation". A term's parametricity theorem then falls straight out of its type.

That is the "for free" part: the theorem is a gift attached to the signature. The intuition is the same each time — a parametric function treats the elements as opaque black boxes, so relabelling every element with f before it runs, or after, cannot change which positions survive or move; only the labels differ. Parametricity turns that "it can't peek" into a rigorous equation.

Checking a free theorem empirically

A free theorem is a proof, not a test — but seeing it hold is bracing. The naturality law for any position-based list function m says \mathsf{map}\,f \,(m\,xs) = m\,(\mathsf{map}\,f\,xs): mapping f then reshaping equals reshaping then mapping. Below we take several genuinely parametric m's (reverse, drop-first, every-other) and a non-trivial f, and check both sides agree — then show a non-parametric function (one that peeks at values) breaking the law. Press Run.

const map = <A, B>(f: (a: A) => B, xs: A[]): B[] => xs.map(f); const eq = (a: number[], b: number[]): boolean => a.length === b.length && a.every((x, i) => x === b[i]); // Parametric list-to-list functions: they only reorder/select by POSITION, // never inspecting the element values. Their free theorem: map f . m == m . map f const reverse = <A>(xs: A[]): A[] => [...xs].reverse(); const dropFirst = <A>(xs: A[]): A[] => xs.slice(1); const everyOther = <A>(xs: A[]): A[] => xs.filter((_, i) => i % 2 === 0); const f = (x: number): number => x * 10 + 1; // any element function const xs = [1, 2, 3, 4, 5]; for (const [name, m] of [["reverse", reverse], ["dropFirst", dropFirst], ["everyOther", everyOther]] as const) { const lhs = map(f, m(xs)); // map f then reshape const rhs = m(map(f, xs)); // reshape then map f console.log(name.padEnd(10), "map f ∘ m =", JSON.stringify(lhs), " m ∘ map f =", JSON.stringify(rhs), " →", eq(lhs, rhs) ? "FREE THEOREM HOLDS" : "FAILS"); } // A NON-parametric function that inspects values (keeps evens). It has type // (number[] -> number[]), NOT ∀α. [α] -> [α] — so parametricity does NOT apply, // and the naturality law can break: const keepEven = (xs: number[]): number[] => xs.filter((x) => x % 2 === 0); const lhs = map(f, keepEven(xs)); const rhs = keepEven(map(f, xs)); console.log("keepEven ", "map f ∘ m =", JSON.stringify(lhs), " m ∘ map f =", JSON.stringify(rhs), " →", eq(lhs, rhs) ? "holds" : "BREAKS (not parametric!)");

Every parametric reshaper obeys the law on the nose; keepEven breaks it, because it looks at the values — so it cannot have the polymorphic type \forall\alpha.\,\mathsf{List}\,\alpha\to\mathsf{List}\,\alpha, and forfeits the free theorem. Behaviour is dictated by type: earn the polymorphic signature and the law is yours; peek at the data and you lose both.

This is the philosophical shock of parametricity, and it is worth sitting with. In an ordinary language, a signature like int f(int) tells you almost nothing — f could square, negate, print to a file, or launch a missile. But \forall\alpha.\,\alpha\to\alpha tells you everything: it must be the identity. The generality of the type is not weakness but strength — the more polymorphic a type is, the fewer functions inhabit it, and so the more the type reveals. Philip Wadler's slogan captures it: the type of a polymorphic function is a theorem, and the function is its proof (the Curry–Howard correspondence made practical). This is why seasoned Haskell and ML programmers "type-driven develop": they write the most general type they can, and let parametricity shrink the space of possible implementations until, often, only the right one is left — sometimes uniquely, so the compiler could all but write the body itself.

Parametricity is a theorem about a pure, total, parametric calculus. Bolt on the features real languages have and the guarantees weaken or vanish. Nontermination: in a language with general recursion, \forall\alpha.\,\alpha\to\alpha is inhabited not only by the identity but by the everywhere-undefined function \bot — the "free" theorems must be restated "up to \bot", and some fail outright. Effects: a function that secretly logs, mutates, or throws is no longer parametric even if its type says \forall\alpha, because the effect can depend on or reveal the value. Haskell's seq is the notorious spoiler — it can observe whether an argument terminates, breaking parametricity and quietly invalidating certain free theorems, which is exactly why adding seq to the language was controversial. And reflection / type-case (Java's instanceof, run-time type information, typeof) destroys parametricity by construction, since the whole point is to inspect the type the theorem assumed opaque. The rule of thumb: a free theorem is only as trustworthy as the parametricity of the language you invoke it in — read the fine print before you bank on it.