Unification
What does it mean to solve a set of constraints? To answer this
question, we define type substitutions. A type substitution is a map
from a type variable to a type. We'll write {t/X}
for the
substitution that maps type variable X
to type t
. The way a
substitution S
operates on a type can be defined recursively:
S(X) = if S = {t/X} then t else X
S(t1 -> t2) = S(t1) -> S(t2)
A substitution S
can be applied to a constraint t = t'
; the result
S(t = t')
is defined to be S(t) = S(t')
. And a substitution can be
applied to a set C
of constraints; the result S(C)
is the result of
applying S
to each of the individual constraints in C
.
Given two substitutions S
and S'
, we write S;S'
for their
composition: (S;S')(t) = S'(S(t))
.
A substitution unifies a constraint t_1 = t_2
if S(t_1) = S(t_2)
.
A substitution S
unifies a set C
of constraints if S
unifies every
constraint in C
. For example, substitution
S = {int->int/Y};{int/X}
unifies constraint X -> (X -> int) = int -> Y
.
To solve a set of constraints C
, we need to find a substitution that
unifies C
. If there are no substitutions that unify C
, where C
is the constraints generated from expression e
, then e
is not
typeable.
To find a substitution that unifies C
, we use an algorithm
appropriately called the unification algorithm. It is defined as
follows:
if
C
is the empty set, thenunify(C)
is the empty substitution.if
C
is the union of a constraintt = t'
with other constraintsC'
, thenunify(C)
is defined as follows, based on that constraint:if
t
andt'
are both the same type variable, e.g.X
, then returnunify(C')
.if
t = X
for some type variableX
, andX
does not occur int'
, then letS = {t'/X}
, and returnunify(S(C'));S
.if
t' = X
for some type variableX
, andX
does not occur int
, then letS = {t/X}
, and returnunify(S(C'));S
.if
t = t0 -> t1
andt' = t'0 -> t'1
, then letC''
be the union ofC'
with the constraintst0 = t'0
andt1 = t'1
, and returnunify(C'')
.if
t = t0 * t1
andt' = t'0 * t'1
, then letC''
be the union ofC'
with the constraintst0 = t'0
andt1 = t'1
, and returnunify(C'')
.if
t = (t0, ..., tn) tc
andt' = (t'0, ..., t'n) tc
for some type constructortc
, then letC''
be the union ofC'
with the constraintsti = t'i
, and returnunify(C'')
.otherwise, fail. There is no possible unifier.
In the second and third subcases, the check that X
should
not occur in t
ensures that the algorithm doesn't produce a cyclic
substitution—for example, {(X -> X) / X}
.
It's possible to prove that the unification algorithm always terminates,
and that it produces a result if and only a unifier actually exists—that
is, if and only if the set of constraints has a solution. Moreover, the
solution the algorithm produces is the most general unifier, in the
sense that if S = unify(C)
and S'
unifies C
, then there
must exist some S''
such that S' = S;S''
.
If R
is the type variable assigned to represent the type of the entire
expression e
, and if S
is the substitution produced by the
algorithm, then S(R)
is the type inferred for e
by HM type
inference. Call that type t
. It's possible to prove t
is the
principal type for the expression, meaning that if e
also has type
t'
for any other t'
, then there exists a substitution S
such that
t' = S(t)
. So HM actually infers the most lenient type that is possible
for any expression.
Let expressions
Consider the following code:
let double f z = f (f z) in
(double (fun x -> x+1) 1, double (fun x -> not x) false)
The inferred type for f
in double
would be X -> X
. In the
algorithm we've described so far, the use of double
in the first
component of the pair would produce the constraint X = int
, and the
use of double
in the definition of b
would produce the constraint X
= bool
. Those constraints would be contradictory, causing unification
to fail!
There is a very nice solution to this called let-polymorphism, which
is what OCaml actually uses. Let-polymorphism enables a polymorphic
function bound by a let
expression behave as though it has multiple
types. The essential idea is to allow each usage of a polymorphic
function to have its own instantiation of the type variables, so that
contradictions like the one above can't happen. We won't cover
let-polymorphism here.