Algorithm W
Algorithm W is a syntax-directed way to construct proofs of these judgments and compute principal types. ([Wikipedia]1)
Algorithm W takes an environment and an expression and returns:
W(Γ, e) = (S, τ)
meaning: under substitution S, e has monotype τ in environment SΓ.
Core machinery:
- Fresh type variables:
newvar(). - Substitutions
S: finite maps from type variables to types; applied everywhere. - Instantiation:
inst(∀α₁…αₙ. τ)replaces eachαᵢwith a fresh type variable. - Generalization:
Gen(Γ, τ), as above. - Unification:
mgu(τ₁, τ₂)gives the most general unifier substitution that makesτ₁andτ₂equal, or fails with a type error. ([Wikipedia]1)
Variable
W(Γ, x):
let σ = Γ(x)
τ = inst(σ)
in (Id, τ)
- Look up
x’s scheme, instantiate to a monotype.
Abstraction λx. e
W(Γ, λx. e):
let α = newvar()
(S, τ) = W(Γ[x ↦ α], e)
in (S, S α -> τ)
xstarts out monomorphic with typeα.- Body may refine
αviaS. - Final function type uses
S α.
Application e1 e2
W(Γ, e1 e2):
let (S1, τ1) = W(Γ, e1)
(S2, τ2) = W(S1 Γ, e2)
β = newvar()
S3 = mgu(S2 τ1, τ2 -> β)
in (S3 ∘ S2 ∘ S1, S3 β)
- Infer function and argument types.
- Force
τ1to be “a function fromτ2to β”. - Unification (
mgu) emits the equality constraints and solves them.
Let-binding let x = e1 in e2
W(Γ, let x = e1 in e2):
let (S1, τ1) = W(Γ, e1)
σ = Gen(S1 Γ, τ1)
(S2, τ2) = W(S1 Γ [x ↦ σ], e2)
in (S2 ∘ S1, τ2)
- Infer the binding’s type.
- Generalize to a scheme (binds the free type vars of
τ1). - Use that scheme when inferring the body.
This explicit threading of substitutions is what distinguishes W from Milner’s earlier “Algorithm J”; W is designed to be easier to reason about formally. ([Wikipedia]1)
Example: let id = λx. x in (id 3, id True)
This showcases:
- Type variables as unknowns,
- Generalization and binding,
- Re-instantiation,
- Unification.
let structure
Expression:
e = let id = λx. x in (id 3, id True)
We start from an environment where literals have known types:
Γ0 = { 3 : Int, True : Bool }
We compute W(Γ0, e) via the let rule:
(S1, τ1) = W(Γ0, λx. x)σ = Gen(S1 Γ0, τ1)(S2, τ2) = W(S1 Γ0 [id ↦ σ], (id 3, id True))- Result:
(S2 ∘ S1, τ2)
W(Γ0, λx. x)
Abstraction case:
-
Fresh
αforx. -
Extend
Γ0toΓ1 = Γ0[x ↦ α]. -
Infer the body
x:-
W(Γ1, x):Γ1(x) = α(monotype; think scheme∀. α).inst(α) = α.- So
(Id, α).
-
-
For the lambda:
(S = Id, τ_body = α)- Function type =
α -> α
So:
W(Γ0, λx. x) = (Id, α -> α)
Thus S1 = Id, τ1 = α -> α.
generalize id
Generalize τ1 against S1 Γ0 (still Γ0):
free(τ1) = {α}free(Γ0) = ∅- So
Gen(Γ0, α -> α) = ∀α. α -> α
Set:
σ_id = ∀α. α -> α
Γ' = Γ0[id ↦ σ_id]
This is exactly where α becomes bound in the type scheme.
infer (id 3, id True)
We treat (e1, e2) as an expression whose type is τ1 × τ2:
- Compute
W(Γ', id 3)→(S_left, τ_left) - Compute
W(S_left Γ', id True)→(S_right, τ_right) - Pair type is
S_right τ_left × τ_right.
W(Γ', id 3)
Application:
-
First,
W(Γ', id):Γ'(id) = ∀α. α -> α- Instantiate:
β -> βwith freshβ. (Id, β -> β)
-
Then,
W(Γ', 3):- Literal:
(Id, Int)
- Literal:
-
Application:
-
Fresh
γfor result. -
Unify
β -> βwithInt -> γ:- Domain:
β ~ Int - Codomain:
β ~ γ
- Domain:
-
So substitution
S3 = { β ↦ Int, γ ↦ Int }.
-
The type of id 3 is Int, with substitution S_left = S3.
W(S_left Γ', id True)
The generalized scheme for id is still ∀α. α -> α (schemes aren’t rewritten by substitutions; they’re re-instantiated).
-
W(S_left Γ', id):- Instantiation again, but with fresh
δ:δ -> δ.
- Instantiation again, but with fresh
-
W(S_left Γ', True):- Literal:
(Id, Bool).
- Literal:
-
Application:
-
Fresh
εfor result. -
Unify
δ -> δwithBool -> ε:δ ~ Boolδ ~ ε
-
Substitution
S' = { δ ↦ Bool, ε ↦ Bool } = S_right.
-
So id True has type Bool.
Pair type
Combine substitutions (S_pair = S_right ∘ S_left) and types:
- Left:
Int - Right:
Bool
So the whole expression has type:
(Int, Bool)
That’s the principal type inferred by Algorithm W.
Where Algorithm W says “no”: λx. x x
As a quick sanity check: λx. x x should not be typable in HM.
Sketch:
-
Give
xtypeα. -
In body
x x:xas function ⇒ typeα.xas argument ⇒ typeα.- Application asks to unify
αwithα -> β(for some β).
-
This triggers an occurs check:
αappears insideα -> β. Unifying would yield an infinite typeα = α -> β, which HM forbids.
So Algorithm W rejects λx. x x with a type error during unification.
Footnotes
-
https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system “Hindley–Milner type system” ↩ ↩2 ↩3