Hindley-Milner
Hindley–Milner is a type system for the λ-calculus with parametric polymorphism. It has two key properties:
- It can infer types without annotations.
- It computes a principal type: the most general type from which all other valid types follow by specialization. ([Wikipedia]1)
HM Types
We distinguish:
Monotypes (τ)
Monotypes (simple types) are built from:
- Type constants:
Int,Bool,String, … - Type variables:
α,β,γ, … - Type constructors: function, lists, pairs, etc.
Typical grammar:
τ ::= α
| Int | Bool | ...
| τ -> τ
| List τ
| τ × τ -- pairs, etc.
Polytypes / type schemes (σ)
Polytypes (type schemes) allow universal quantification over type variables:
σ ::= τ
| ∀α. σ -- usually written ∀α₁…αₙ. τ
Example:
∀α. α -> αis the type of the polymorphic identity function.Int -> Intis a monomorphic specialization of that scheme.
In the environment Γ, variables map to schemes (polytypes), not bare monotypes. ([Wikipedia]1)
Type Variables
During inference: Unknowns
When you don’t yet know the type of something, you introduce a fresh type variable:
- For
λx. x:- Give
xa fresh typeα. - The body
xalso has typeα. - The function has type
α -> α.
- Give
At this stage, α is an inference variable: a placeholder to be solved later.
After Generalization: Parameters of a scheme
At a let, HM generalizes a monotype into a scheme by quantifying over its free type variables that aren’t pinned down by the environment:
Gen(Γ, τ) = ∀α₁…αₙ. τ
where {αᵢ} = free(τ) \ free(Γ)
Example:
let id = λx. x in ...
- Infer
id : α -> α. - Generalization yields
id : ∀α. α -> α.
Now α is a bound type variable in the scheme, exactly like a λ-bound term variable: it has scope within the body of the type.
When you use id, you:
- Instantiate
∀α. α -> αto, say,β -> βwith a freshβ. - Later unification may force
β = Int,β = Bool, etc.
So:
- Inference variables: “unknowns we’re solving for”.
- Quantified variables: “parameters of a polymorphic function”.
Constraints and “Bounds” on Type Variables
Bound v. Free
A type variable is bound if it appears under a ∀:
- In
∀α. α -> α,αis bound. - In
∀α. α -> β,αis bound,βis free.
Generalization at let takes free inference variables and turns them into bound quantified parameters.
This is purely syntactic, like “bound vs free variables” in λ-calculus.
Semantic Bounds: restricted instantiations (constrained HM)
Basic HM only has equality constraints between types (unification). But real languages often add an extra layer: constrained polymorphism, which you can think of as “bounds on type variables”.
Type schemes become:
∀α₁…αₙ. C ⇒ τ
where C is a set of constraints on the type variables—e.g. Eq α, Num α. This is the HM(X) framework: HM plus a parametric constraint system X that’s solved alongside unification. ([Tufts Computer Science]2)
Example (Haskell style):
show :: Show a => a -> String
Read as:
∀a. Show a ⇒ a -> String
Here a is:
- Bound by
∀a. - Semantically bounded by the predicate
Show a: you can only instantiateawith types that satisfy theShowconstraint.
So:
-
Plain HM:
- Constraints are equations between types:
τ₁ ~ τ₂. - Bounds = “is this variable under a ∀?”. Unification just figures out equalities.
- Constraints are equations between types:
-
Constrained HM / HM(X):
- Constraints are logical predicates over types (
Eq α,Num α, record rows, effects, etc.). - Bounds = “α must satisfy constraint C when instantiated”.
- Constraints are logical predicates over types (
For your own LSP/typechecker, this is where you decide if you want:
- A pure Damas–Milner ([EECS Berkeley]3) core (just unification constraints), or
- A constrained layer (e.g. traits/typeclasses/effects) integrated via HM(X)-style rules.
Typing Rules
You can present HM via a judgment:
Γ ⊢ e : τ
with Γ mapping variables to schemes. The important rules (up to let) are:
-
Var: If
x : σ ∈ Γ, then- instantiate
σto monotypeτ, - conclude
Γ ⊢ x : τ.
- instantiate
-
Abs (λ): For
λx. e:- give
xa fresh typeα, - infer
Γ, x:α ⊢ e : τ, - result type is
α -> τ.
- give
-
App (application): For
e₁ e₂:- infer
Γ ⊢ e₁ : τ₁,Γ ⊢ e₂ : τ₂, - assert
τ₁must beτ₂ -> βfor freshβ, - unify
τ₁withτ₂ -> β, giving substitutionS, - result type is
S β.
- infer
-
Let: For
let x = e₁ in e₂:- infer
Γ ⊢ e₁ : τ₁, - generalize
τ₁toσ = Gen(Γ, τ₁), - infer
Γ, x:σ ⊢ e₂ : τ₂, - result type is
τ₂.
- infer
Algorithm W
See Algorithm W
Further Reading
- Bernstein, Max. “Damas-Hindley-Milner Inference Two Ways.” Max Bernstein, 15 Oct. 2024, https://bernsteinbear.com/blog/type-inference/.
- Diehl, Stephen. “Hindley-Milner Inference” Write You a Haskell. https://smunix.github.io/dev.stephendiehl.com/fun/006_hindley_milner.html.
- Tuhola, Henri. Hindley-Milner Type System/Algorithm W Study. https://boxbase.org//entries/2018/mar/5/hindley-milner.
- Hazelden, Phil. A Reckless Introduction to Hindley-Milner Type Inference. https://reasonableapproximation.net/2019/05/05/hindley-milner.html.
Footnotes
-
https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system “Hindley–Milner type system” ↩ ↩2
-
https://www.cs.tufts.edu/~nr/cs257/archive/martin-odersky/hmx.pdf “Type Inference with Constrained Types” ↩
-
https://people.eecs.berkeley.edu/~necula/Papers/DamasMilnerAlgoW.pdf “Principal type-schemes for functional programs” ↩