Tese Polymorphic Type Inference
Tese Polymorphic Type Inference
AU AARHUS
UNIVERSITY
DEPARTMENT OF COMPUTER SCIENCE
Abstract
This thesis contributes to the understanding of how the Hindley-Milner Type system works based
on a language that is an extended version of the lambda calculus with let-polymorphism. The
proofs of the consistency between the dynamic semantics and static semantics of the language
are given for the new expressions introduced in the language. In particular, the new expressions
are integers, booleans, strings, tuples, the fst operation, and the snd operation.
Furthermore, two type inference algorithms are discussed, namely, W and Wopt . The latter
algorithm is an optimized version of the prior. Soundness proofs are given for W. Implementations
for both algorithms are given in OCaml.
Finally, the full implementation of both algorithms can be found on our GitHub page here.
ii
Contents
Abstract ii
1 Introduction 1
4 Optimizing Algorithm W 25
4.1 The Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.1.1 Union-Find Data Structure . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.1.2 Unification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.1.3 Pseudocode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.2 Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5 Conclusion 30
Bibliography 31
Appendices 32
iii
A Operations 33
A.1 Tyvars Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
A.2 Substitution Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
A.3 Unification Table for W . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
A.4 Unification Table for Wopt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
B Generalization 35
B.1 Full Derivation of Example 12 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
D Additional Proofs 44
D.1 Consistency Proof for the snd(e1 , e2 ) . . . . . . . . . . . . . . . . . . . . . . . . . 44
D.2 Soundness Proof for snd(e1 , e2 ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
iv
Chapter 1
Introduction
In many programming languages, the programmer has to make sure the types of the variables
are coherent to avoid typing errors. One way to make it easier for the programmer to work with
the types is to infer the types so the programmer can omit explicit type annotation. A compiler
will then be able to deduce which variables have which types and an editor might even tell the
programmer the type of different variables. This can give a better overview of the code and the
programmer can receive on-the-fly error messages about types where the expected type can be
mentioned. This can potentially make it easier to figure out how to fix errors. One problem
with type inference algorithms is that it might be harder to infer the type in more complex
languages. The Hindley-Milner type system is a type system based on lambda calculus and is
complex enough to have polymorphic functions in the form of let polymorphism while being
simple enough to have a practical type inference algorithm that can give the programmer hints
in real-time while writing their code.
In Chapter 2 of this thesis, we will introduce the Hindley-Milner type system by defining the
grammar of the language and present both the dynamic and static semantics. We will go
over a few concepts that will help us build the algorithm such as substitutions, instantiation,
generalization, and unification. After this, a proof of the consistency between the dynamic
semantics and the static semantics will be presented. We will provide both pseudocode and
a full working implementation of the type inference algorithm known as algorithm W as well
as proof of the soundness of the algorithm in Chapter 3. In Chapter 4 we will then look into
optimizing algorithm W by using a Union-Find data structure instead of substitutions to which
we have also provided both pseudocode and a full working implementation. In the appendices of
this thesis, we have summaries of some operations, examples of type inference, more detailed
examples, additional proofs, and some extra practical code such as tests and pretty printers for
both algorithms.
1
Chapter 2
In order to reason about type systems and type inference, we will have to define a language
and its semantics. The language we will consider in this thesis is a relatively small functional
language. In this chapter, we will formally define the language by defining its grammar and
semantics. Lastly, we will prove consistency between the two groups of semantics..
x ∈ Var = {a, b, . . . , x, y, . . . }
i∈Z
b ∈ Boolean = {true, f alse}
s ∈ String = {”myV ariable”, ”myV ariable2”, . . .}
The grammar of the language Exp ranged over by e is defined using a context-free grammar
where the production rules are in Backus-Naur form
⟨e⟩ ::= i Integer
| b Boolean
| s String
| x Variable
| λx.e1 Lambda Abstraction
| e1 e2 Application
| let x = e1 in e2 Let
| (e1 , e2 ) Tuple
| fst (e1 , e2 ) First Tuple
| snd (e1 , e2 ) Second Tuple
note that this grammar is an extended version of the lambda calculus with let-polymorphism.
In particular, we have extended the grammar with the first three production rules and the last
three production rules. Parentheses are used to disambiguate expressions.
2
2.2 Semantics
In this section, we present the semantics of the language. The semantics of this language can be
separated into two groups, namely the dynamic semantics and the static semantics. The former
deals with the evaluation of programs while the latter deals with type checking programs.
Figure 2.1 presents the semantic objects being used in the inference rules. Note that the result
wrong is not a value but merely an indicator of a nonsensical evaluation, for instance, trying to
do the fst operation on a lambda abstraction.
Each inference rule has the general form
P1 , . . . , P n
n≥0
C
where each premise Pi for 0 ≤ i ≤ n all together allows us to infer the conclusion C. Each
premise is either a sequent or a side condition written by standard mathematical concepts.
The inference rules for the dynamic semantics are the following
D-App
E ⊢ e1 → [x0 , e0 , E0 ]
E ⊢ e2 → v0
E0 ± {x0 7→ v0 } ⊢ e0 → r
E ⊢ e1 e2 → r
D-App-Wrong1 D-App-Wrong2
E ⊢ e1 → [x0 , e0 , E0 ] E ⊢ e2 → wrong E ⊢ e1 → w : w ∈ (Val \ Clos) ∪ {wrong}
E ⊢ e1 e2 → wrong E ⊢ e1 e2 → wrong
D-Let D-Let-Wrong
E ⊢ e1 → v1 E ± {x 7→ v1 } ⊢ e2 → r E ⊢ e1 → wrong
E ⊢ let x = e1 in e2 → r E ⊢ let x = e1 in e2 → wrong
3
D-Tuple D-Tuple-Wrong1 D-Tuple-Wrong2
E ⊢ e 1 → v1 E ⊢ e2 → v2 E ⊢ e1 → wrong E ⊢ e 1 → v1 E ⊢ e2 → wrong
E ⊢ (e1 , e2 ) → (v1 , v2 ) E ⊢ (e1 , e2 ) → wrong E ⊢ (e1 , e2 ) → wrong
D-Fst D-Snd
E ⊢ (e1 , e2 ) → (v1 , v2 ) E ⊢ (e1 , e2 ) → (v1 , v2 )
E ⊢ snd(e1 , e2 ) → v1 E ⊢ snd(e1 , e2 ) → v2
The notation E ⊢ e → r is a ternary relation between elements of Env, Exp, and Results,
respectively. One can read it as – Under the Environment E when e is evaluated it results in
r. The domain and range of any map f are denoted Dom f and Range f respectively. For any
f, g that is an infinite or finite map we define f ± g to be the map where f is modified by g.
The resulting map will have domain Dom f ± g = Dom f ∪ Dom g and range Range f ± g =
Range g ∪ {f (x) : x ∈ Dom f ∧ x ∈ / Dom g}. We use ± because the + resembles that the
domain can possibly be larger than f and the − because if f, g have the same element in their
domains then the value in the range of g is used hence a value might disappear from f .
S-Let
Γ ⊢ e1 : τ1 Γ ± {x 7→ ClosΓ τ1 } ⊢ e2 : τ
Γ ⊢ let x = e1 in e2 : τ
4
However, due to the extension of the grammar, we have to define one rule per new expression
Now that the type system is well-defined we can pick parts of the rules (e.g. the ClosΓ τ1 ) in
order to explain their meanings.
2.2.2.1 Types
Let TyCon be a finite set of nullary type constructors also known as the basic types. A nullary
type constructor simply means that the type itself takes zero types as parameter. Furthermore,
let TyVar be an infinite set of type variables.
τ ::= π | α | τ1 → τ2 | τ1 × τ2
σ ::= τ | ∀α.σ1
Example 3 (Type schemes). Let σ1 , σ2 be type schemes and σ1 = ∀α.α → α and σ2 = int → int
Additionally, there is the concept of bound and free type variables in σ. If σ = ∀α1 , . . . , αn .τ
then all {α1 , . . . , αn } are said to be bound in σ. Contrarily, a type variable α is free if it is not
bound and occurs in τ .
Example 4 (Free and bound type variables). Let σ = ∀β.(β → bool) × (string → γ) then {γ}
is a free variable in σ and {β} is a bound variable in σ.
5
2.2.2.2 Type Environment
A type environment Γ contains information about which program variables have which type
schemes. Thus, it is a finite map from program variables to type schemes i.e. Γ : Var →
TypeScheme. Lastly, a judgement of the form Γ ⊢ e : τ is read as follows — Under the type
environment Γ the expression e is well-typed with τ .
tyvars(σ) = tyvars(∀α1 . . . αn .τ )
n
[
= tyvars(τ ) \ αi
i=1
Example 6 (The tyvars map applied on a type environment). Let Γ = {x 7→ ∀α, γ.(α × β) × γ}
then
6
Furthermore, whenever we write fresh type variables we mean type variables that do not exist
in the tyvars(Γ) set nor in any of the bound variables in the range of Γ. Finally, a type τ is a
monotype µ ∈ Type if tyvars(τ ) = ∅.
2.2.2.4 Substitutions
A substitution S is a map from a type variable to a type, S : TyVar → Type. We let the identity
substitution be denoted ID. Moreover, every substitution is the identity substitution outside its
domain and range. A substitution is ground if for all types τ in the range of any substitution S is
a monotype. Similarly to the last section, we will define the substitution operations. Parentheses
can be disregarded.
S(τ )
By case distinction of τ we find out what the result of the operation would be.
τ =π
The result of the substitution applied to the nullary type constructor π is π.
τ =α
If {α 7→ τ1 } ⊆ S then Sα = τ1 else Sα = α.
Example Let S = {β 7→ γ}. If α = β then Sα = Sβ = γ. However, if α = γ then
Sα = Sγ = γ.
τ = τ1 → τ2
The result of the substitution applied to an abstraction is defined as Sτ1 → Sτ2 . For
the sake of proofs introduced later we explicitly write it as a definition
Definition 2.2.1. For any substitution S and any type of the form τ1 → τ2 then
S(τ1 → τ2 ) = Sτ1 → Sτ2
τ = τ1 × τ2
The result of the substitution applied to a tuple is defined as Sτ1 × Sτ2 . For the sake
of proofs introduced later we explicitly write it as a definition
Definition 2.2.2. For any substitution S and any type of the form τ1 × τ2 then
S(τ1 × τ2 ) = Sτ1 × Sτ2
See Appendix A for a summary of the substitution operations on types.
S(σ)
For this operation, we will use the notation {αi 7→ βi } = {α1 7→ β1 , . . . , αn 7→ βn } where n is
the number of bound variables in the respective σ. We would like to define Sσ such that the
following definition holds for σ1 = σ and σ2 = Sσ.
Definition 2.2.3. Let σ1 = ∀α1 . . . αn .τ1 and σ2 = ∀β1 . . . βn .τ2 be type schemes and S be a
S
substitution. We will write σ1 −
→ σ2 if all of the following holds
1. m = n
3. (S ± {αi 7→ βi })τ1 = τ2
7
Definition 2.2.4. Given a substitution S, a typescheme σ = ∀α1 . . . αn .τ , and n fresh variables
β1 , . . . , βn , Sσ = S(∀α1 . . . αn .τ ) = ∀β1 . . . βn .(S ± {αi 7→ βi })τ
S
Lemma 2.2.1. For any typescheme σ and any substitution S, σ −
→ Sσ
Proof of lemma 2.2.1. The first condition obviously holds since we are using 1, . . . , n for the
numbering of the bound variables in both σ and Sσ. The second condition holds since βi , . . . , βn
are fresh type variables and thus are not present anywhere else. We see that in our case τ1 = τ
and τ2 = (S ± {αi 7→ βi })τ1 which is exactly what the third condition requires. Thus, the third
condition holds as well.
Sσ = S(∀α1 .α → α1 )
= ∀β1 .(S ± {α1 7→ β1 })(α → α1 )
= ∀β1 .({α 7→ bool, α1 7→ int} ± {α1 7→ β1 })(α → α1 )
= ∀β1 .{α 7→ bool, α1 7→ β1 }(α → α1 )
= ∀β1 .bool → β1
Note that the ± operator prioritizes the second operand which is why we get {α 7→ bool, α1 7→
int} ± {α1 7→ β1 } = {α 7→ bool, α1 7→ β1 }.
S(Γ)
Similarly to Sσ, we would like to define SΓ such that the following definition holds for Γ1 = Γ
and Γ2 = SΓ.
S
Definition 2.2.5. Let Γ1 , Γ2 be type environments. We write Γ1 −
→ Γ2 if the following holds
1. Dom Γ1 = Dom Γ2
S
2. ∀x ∈ Dom Γ1 , Γ1 (x) −
→ Γ2 (x)
S
Lemma 2.2.2. For any type environment Γ and any substitution S, Γ −
→ SΓ
Proof of lemma 2.2.2. The lemma is a special case of 2.2.5. By definition 2.2.6 we have Dom SΓ
which should be equal to Dom Γ so the first condition holds. We see that the second condition
S S
in our case is Γ(x) −
→ SΓ(x) which by our definition is the same as Γ(x) − → S(Γ(x)). Since Γ is
S
a map to type schemes we get σ −
→ Sσ for some σ and therefore the second condition must hold
by lemma 2.2.1.
8
Example 8 (Substitution on a type environment). Let Γ = {x 7→ ∀α1 .α → α1 , y 7→ ∀α1 , α2 .α →
int} and S = {α 7→ int} then
SΓ = {x 7→ S(Γ(x))} ∪ {y 7→ S(Γ(y))}
= {x 7→ S(∀α1 .α → α1 )} ∪ {y 7→ S(∀α1 , α2 .α → int)}
= {x 7→ ∀β1 .int → β1 } ∪ {y 7→ ∀β1 , β2 .int → int} By definition 2.2.4
= {x 7→ ∀β1 .int → β1 , y 7→ ∀β1 , β2 .int → int}
Sn . . . S2 S1
With the operations being defined we look at the composition of substitutions. It is is defined
generally as follows
[
Sn . . . S2 S1 = {α 7→ Sn . . . S2 τ }
α∈Dom S1 ,
τ =S1 α
The order of evaluation is from right to left. For instance, for three substitutions S1 , S2 , S3 then
for any type τ then S3 S2 S1 τ = S3 (S2 (S1 τ )).
S1 = {α 7→ β}
S2 = {β 7→ γ}
S3 = {γ 7→ δ}
S3 S2 S1 = {α 7→ S3 S2 β}
= {α 7→ S3 γ}
= {α 7→ δ}
2.2.2.5 Instantiation
The > operator in the S-Lookup rule indicates that a type τ1 is an instantiation of a type scheme
σ = ∀α1 . . . αn .τ2 written as σ > τ1 . More specifically, τ1 is an instantiation of σ if there exists a
substitution S with domain {α1 , . . . , αn } and range {β1 , . . . , βn } where each of the type variables
in the range are fresh such that τ1 = Sτ2 .
2.2.2.6 Generalization
In the S-Let rule, the ClosΓ operation on any type τ is defined as ClosΓ τ = ∀α1 , . . . , αn .τ where
{α1 , . . . , αn } is defined as tyvars(τ ) \ tyvars(Γ). Thus, this operation tries to generalize over each
type variable in τ but only if it does not already exist in the free type variables of Γ.
9
Example 11 (Simple generalization). If τ = α → α and Γ = ∅ then to compute ClosΓ τ we first
compute tyvars(τ ) \ tyvars(Γ) = {α} \ ∅ = {α}. Thus, ClosΓ τ = ∀α.α → α.
which means we can quantify τ with α, β, δ and thus ClosΓ τ = ∀α, β, δ.α → (int × β) →
(γ × string) → δ → int. See Appendix B for a more detailed derivation.
Definition 2.3.1. We say that v has monotype µ written ⊨ v : µ if one of the following holds
• v = bv and bv has the correct type i.e. for instance for bv = ”myV ariable” then µ = string etc.
Definition 2.3.2. Let Γ◦ be a type environment that ranges over all closed type environments
then we define the following
We present consistency proofs for tuples, fst, and snd. The proofs for the other expressions can
be found in [4] and the base cases for integers, booleans, and strings are trivial and therefore
omitted.
10
Proof of theorem 2.3.1. By structural induction on e.
e = (e1 , e2 )
The type inference of the expression must have been of the form
S-Tuple
Γ ⊢ e1 : τ1 Γ ⊢ e2 : τ2
(2.2)
Γ ⊢ (e1 , e2 ) : τ1 × τ2
D-Tuple
E ⊢ e1 → v1 E ⊢ e 2 → v2
(2.3)
E ⊢ (e1 , e2 ) → (v1 , v2 )
D-Tuple-Wrong1
E ⊢ e1 → wrong E ⊢ e 2 → v2
(2.4)
E ⊢ (e1 , e2 ) → wrong
D-Tuple-Wrong2
E ⊢ e 1 → v1 E ⊢ e2 → wrong
(2.5)
E ⊢ (e1 , e2 ) → wrong
By our I.H. and 2.1, the first premise in 2.2, (2.3 / 2.4 / 2.5) we get
r1 ̸= wrong (2.6)
⊨ r1 : τ1 (2.7)
Furthermore, by our I.H and 2.1, the second premise in 2.2, (2.3 / 2.5) we get
r2 ̸= wrong (2.8)
⊨ r2 : τ2 (2.9)
This means the evaluation cannot have been 2.5. The evaluation must have been 2.3, meaning
that r1 = v1 , r2 = v2 , and r = (v1 , v2 ) ∈ Val × Val = Val. Since a Val cannot be wrong then
r ̸= wrong.
From 2.7 and 2.9 we have that ⊨ r1 : τ1 and ⊨ r2 : τ2 respectively. Since r = (v1 , v2 ) = (r1 , r2 )
and ⊨ (r1 , r2 ) : τ1 × τ2 , we must have that ⊨ r : τ1 × τ2 .
e = fst(e1 , e2 )
The type inference of the expression must have been of the form
11
S-Fst
Γ ⊢ (e1 , e2 ) : τ1 × τ2
(2.10)
Γ ⊢ fst(e1 , e2 ) : τ1
D-Fst
E ⊢ e 1 → v1 E ⊢ e 2 → v2
(2.11)
E ⊢ fst(e1 , e2 ) → v1
D-Fst-Wrong1
E ⊢ e1 → wrong E ⊢ e2 → v2
(2.12)
E ⊢ fst(e1 , e2 ) → wrong
D-Fst-Wrong2
E ⊢ e 1 → v1 E ⊢ e2 → wrong
(2.13)
E ⊢ fst(e1 , e2 ) → wrong
We see that e1 → r1 and e2 → r2 for some r1 , r2 ∈ Results.
r1 ̸= wrong (2.14)
⊨ r1 : τ1 (2.15)
r2 ̸= wrong (2.16)
⊨ r2 : τ2 (2.17)
This means the evaluation cannot have been 2.13. The evaluation must have been 2.11, meaning
that r1 = v1 , r2 = v2 , and r = v1 ∈ Val. Since a Val cannot be wrong then r ̸= wrong.
e = snd(e1 , e2 )
Similar to the e = fst(e1 , e2 ) case and thus omitted here. However, the proof for this case can be
found in Appendix D.
12
Chapter 3
With the language and its semantics being defined we will now dig into how an actual implementa-
tion of a type inference algorithm based on the language and its semantics from Chapter 2 works.
In this chapter, we will present an inefficient type inference algorithm and soundness proofs for
it. We will not provide completeness proof for the algorithm but in [1] they are presented for the
lambda calculus with let-polymorphism.
3.1.1 Unification
The idea of unification is to unify two types τ1 , τ2 i.e. making them equivalent to each other. In
W, the unification makes use of substitutions. Particularly, unification in the algorithm is defined
as Unify : (Type × Type) → Substitution where the range is the set of all substitutions.
The possible operations for Unify are defined below
Unify(π1 , π2 )
If π1 = π2 then we return the empty substitution ID, otherwise we fail.
Unify(α1 , α2 )
If α1 = α2 then we return the empty substitution ID otherwise we make α1 equal to α2 and thus
return {α1 7→ α2 }.
Unify(α, τ )
Unify(τ, α)
13
The two operations above do the following. First, we get S1 = Unify(τ11 , τ21 ) then S2 =
Unify(S1 τ21 , S1 τ22 ). If both succeed then the substitution composition S2 S1 is returned.
In all other possible cases the unification fails. The operations’ semantics are taken from [3]. One
thing to note is the criteria we have in the Unify(α, τ ) and Unify(τ, α) operations. The criteria
is needed to prevent creation of infinite types. For instance, consider τ = α × α then Unify(α, τ )
must fail since there is no finite type solving the symbolic equation α = (α × α).
Example 13 (Unification). Let the Unify function take the following two types as parameter
τ1 = int → α and τ2 = int → (β × γ) then
3.1.2 Pseudocode
The pseudocode for W is presented below
14
3.2 Soundness Proof
In this section we prove the soundness of W for each expression in our grammar. W is sound in
the following sense
e=i
By filling in 3.1, 3.2 in 3.2.1 we get that we want to prove IDΓ ⊢ i : int.
By letting Γ = IDΓ1 we have IDΓ ⊢ i : int which is exactly what we wanted to prove.
e=b
By filling in 3.4, 3.5 in 3.2.1 we get that we want to prove IDΓ ⊢ b : bool.
By our S-Bool rule we have IDΓ ⊢ b : bool which is what we wanted to show.
e=s
15
By lemma 2.2.2 where Γ = Γ and S = ID
ID
Γ −−→ IDΓ (3.7)
By filling in 3.6, 3.7 in 3.2.1 we get that we want to prove IDΓ ⊢ s : string
By our S-String rule we have IDΓ ⊢ s : string which is what we wanted to show.
e=x
ID Γ ⊢ x : {αi 7→ βi }τ (3.10)
if x ∈
/ Dom Γ then fail else let ∀α1 . . . αn .τ = Γ(x) (3.12)
Let us consider the two cases — x ∈/ Dom Γ and x ∈ Dom Γ. Assume x ∈ / Dom Γ then by
3.12 W fails. However, by assumption W succeeds (by Theorem 3.2.1) and therefore we have a
contradiction. Thus, x ∈ Dom Γ. Furthermore, we have from the algorithm
β1 . . . βn be new (3.13)
Thus, we have that the two premises hold and by the conclusion from 3.11 we have IDΓ ⊢ x :
{αi 7→ βi }τ which is what we wanted to prove.
e = λx.e1
S1 (Γ ± {x 7→ α}) ⊢ e1 : τ1 (3.16)
16
By applying the substitution S1 on α and Γ we get
S1 Γ ± {x 7→ S1 α} ⊢ e1 : τ1 (3.17)
e = e1 e2
We want to show S3 S2 S1 Γ ⊢ e1 e2 : S3 α.
S2 S1 Γ ⊢ e1 : S2 τ1 (3.25)
S3 S2 τ1 = S3 (τ2 → α) (3.27)
By definition 2.2.1 and the RHS of the equality sign in 3.27 we have
S3 S2 τ1 = S3 (τ2 → α) = S3 τ2 → S3 α (3.28)
17
By lemma 3.2.2 and 3.25, 3.29 we have
S3 S2 S1 Γ ⊢ e1 : S3 S2 τ1 (3.30)
By the equality presented in 3.28 and by replacing the type in 3.30 we get
S3 S2 S1 Γ ⊢ e1 : S3 τ2 → S3 α (3.31)
S3 S2 S1 Γ ⊢ e2 : S3 τ2 (3.32)
Thus, by letting 3.31 and 3.32 be the premises of the S-App rule
S-App
Γ ⊢ e1 : τ ′ → τ Γ ⊢ e2 : τ ′
Γ ⊢ e1 e2 : τ
e = (let x = e1 in e2 )
By definition of the Closτ operation it can not make the algorithm stop and we can proceed
without any issues. Thus, by lemma 2.2.2 where Γ = S1 Γ ± {x 7→ ClosS1 Γ τ1 } and S = S2 we
have
S2
S1 Γ ± {x 7→ ClosS1 Γ τ1 } −→ S2 (S1 Γ ± {x 7→ ClosS1 Γ τ1 }) (3.37)
18
By lemma 3.2.2 and 3.35, 3.40 we have
S2 S1 Γ ⊢ e1 : S2 τ1 (3.41)
e = (e1 , e2 )
S2 S1 Γ ⊢ e1 : S2 τ1 (3.47)
e = fst(e1 , e2 )
19
By lemma 2.2.2 where Γ = Γ and S = S1 we have
S
1
Γ −→ S1 Γ (3.50)
S1 Γ ⊢ (e1 , e2 ) : τ1 × τ2 (3.51)
e = snd(e1 , e2 )
Similar to the e = fst(e1 , e2 ) case and therefore omitted. However, the proof for this case can be
found in Appendix D.
3.3 Implementation
In this section we will implement W for the language defined in Chapter 2. We will make use of
the pseudocode for W defined in this chapter and the language and its semantics.
3.3.1 Code
Our chosen programming language for the implementation is OCaml. It is based on the Hindley-
Milner type system which makes it an obvious candidate for another language based on the same
type system. The relevant files for W are presented in the following
20
The types defined in the Section 2.2.2.1 are The grammar defined in Section 2.1 is im-
implemented as follows plemented as an AST (Abstract Syntax
Tree)
types.ml
ast.ml
module SS = Set.Make(Int)
open Types
exception Fail of string
type bas_val =
type tyvar = int | Int of int
| Bool of bool
| String of string
type tycon =
| Int
type exp =
| Bool
| BasVal of bas_val
| String
| Var of program_variable
| Lambda of { id : program_variable;
type typ =
e1 : exp }
| TyCon of tycon | App of { e1 : exp; e2 : exp }
| TyVar of tyvar | Let of { id : program_variable;
| TyFunApp of {t1: typ; t2: typ} e1 : exp; e2 : exp }
| TyTuple of {t1: typ; t2: typ} | Tuple of { e1 : exp; e2 : exp }
| Fst of exp
type typescheme = | Snd of exp
TypeScheme of {tyvars: SS.t; tau: typ}
21
The type environment Γ defined in Section The substitution S and its operations as de-
2.2.2.2 is implemented as follows fined in Section 2.2.2.4 are implemented as fol-
lows
typeEnv.ml
substitution.ml
open Types
open Types
module Gamma =
Map.Make (struct module TE = TypeEnv
type t = program_variable
let compare = compare module Substitution =
end) Map.Make (struct
type t = tyvar
type ts_map = typescheme Gamma.t let compare = compare
end)
let wrap_monotype tau =
TypeScheme {tyvars=SS.empty; tau} type map_type = typ Substitution.t
let empty: map_type = Substitution.empty
let empty = Gamma.empty let add k v t : map_type =
Substitution.add k v t
let add k v t = Gamma.add k v t let look_up k t: typ option =
Substitution.find_opt k t
let look_up k t = Gamma.find_opt k t let remove k t: map_type =
Substitution.remove k t
let remove k t = Gamma.remove k t let get_or_else k t ~default =
match look_up k t with
let bindings t = Gamma.bindings t | Some v -> v
| None -> default
let map m t = Gamma.map m t let apply t typ: typ =
let counter = ref 0 let rec subst typ' =
let get_next_tyvar () = match typ' with
counter := !counter + 1; | TyCon _ -> typ'
!counter | TyVar tv ->
let reset () = counter := 0 get_or_else tv t ~default:typ'
| TyFunApp {t1; t2} ->
A small helper file TyFunApp {t1 = subst t1; t2 = subst t2}
| TyTuple {t1; t2} ->
utils.ml TyTuple {t1 = subst t1; t2 = subst t2}
in
open Types
subst typ
let apply_to_typescheme t
module TE = TypeEnv
(TypeScheme{tyvars; tau}) =
TypeScheme{tyvars; tau=apply t tau}
let new_tyvar () = TyVar (TE.get_next_tyvar())
let apply_to_gamma t gamma =
let ( +- ) gamma (id, ts) = TE.add id ts
TE.map (apply_to_typescheme t) gamma
gamma
let map m (t: map_type) = Substitution.map m t
let ( !& ) t = TE.wrap_monotype t
let union a b c =
let ( => ) t1 t2 = TyFunApp { t1; t2 }
Substitution.union a b c
let ( ** ) t1 t2 = TyTuple { t1; t2 }
let compose (s2: map_type) (s1: map_type) =
union (fun _ _ v2 -> Some v2) s2
let combine_sets sets =
(map (fun v -> apply s2 v) s1)
List.fold_left
let bindings t = Substitution.bindings t
(fun a b -> SS.union a b) SS.empty sets
let of_seq s = Substitution.of_seq s
let of_list l = of_seq (List.to_seq l)
let assoc_or_else bindings key ~default =
Option.value (List.assoc_opt key bindings)
~default:default
22
algorithmW.ml let subst = S.of_list bindings in
(S.empty, S.apply subst tau)
open Types
open Utils let infer_type exp =
let rec w gamma exp =
module A = Ast match exp with
module S = Substitution | A.Var id -> (
match TE.look_up id gamma with
let rec find_tyvars tau = match tau with | None -> raise
| TyCon _ -> SS.empty (Fail "id not in type environment")
| TyVar alpha -> SS.singleton alpha | Some ts -> specialize ts)
| TyFunApp { t1; t2 } -> | A.Lambda { id; e1 } ->
SS.union (find_tyvars t1) (find_tyvars t2) let alpha = new_tyvar () in
| TyTuple { t1; t2 } -> let s1, tau1 =
SS.union (find_tyvars t1) (find_tyvars t2) w (gamma +- (id, !&alpha)) e1 in
(s1, S.apply s1 alpha => tau1)
let find_free_tyvars | A.App { e1; e2 } ->
(TypeScheme {tyvars; tau}) = let (s1, tau1) = w gamma e1 in
SS.diff (find_tyvars tau) tyvars let (s2, tau2) =
w (S.apply_to_gamma s1 gamma) e2 in
let clos gamma tau = let alpha = new_tyvar () in
let free_tyvars_tau = find_tyvars tau in let s3 =
let free_tyvars_gamma = unify (S.apply s2 tau1) (tau2 => alpha)
combine_sets (List.map (fun (_, v) -> in (S.compose s3 (S.compose s2 s1),
find_free_tyvars v) (TE.bindings gamma)) in S.apply s3 alpha)
TypeScheme { tyvars = SS.diff | A.Let { id; e1; e2 } ->
free_tyvars_tau free_tyvars_gamma; tau } let (s1, tau1) = w gamma e1 in
let s1_gamma =
let occurs_check tyvar tau = S.apply_to_gamma s1 gamma in
if SS.mem tyvar (find_tyvars tau) then let (s2, tau2) =
raise (Fail "recursive unification") w (s1_gamma +- (id, clos s1_gamma tau1)) e2
in (S.compose s2 s1, tau2)
let rec unify t1 t2 = | A.Tuple { e1; e2 } ->
match t1, t2 with let (s1, tau1) = w gamma e1 in
| TyCon c1, TyCon c2 -> if c1 = c2 then let (s2, tau2) =
S.empty else raise (Fail "cannot unify") w (S.apply_to_gamma s1 gamma) e2 in
| TyVar tv1, TyVar tv2 -> if tv1 = tv2 then (S.compose s2 s1,
S.empty else S.add tv1 t2 S.empty (S.apply s2 tau1) ** tau2)
| TyVar tv, _ -> occurs_check tv t2; | A.Fst e1 -> (
S.add tv t2 S.empty let (s1, tau1) = w gamma e1 in
| _, TyVar tv -> occurs_check tv t1; match tau1 with TyTuple { t1; _ } ->
S.add tv t1 S.empty (s1, t1) | _ -> raise
| TyFunApp { t1 = t11; t2 = t12 }, (Fail "expected tuple"))
TyFunApp { t1 = t21; t2 = t22 } | A.Snd e1 -> (
| TyTuple { t1 = t11; t2 = t12 }, let (s1, tau1) = w gamma e1 in
TyTuple { t1 = t21; t2 = t22 } -> match tau1 with TyTuple { t2; _ } ->
let s1 = unify t11 t21 in (s1, t2) | _ -> raise
let s2 = (Fail "expected tuple"))
unify (S.apply s1 t12) (S.apply s1 t22) in | A.BasVal b ->
S.compose s2 s1 match b with
| _ -> raise (Fail "unify _ case") | Int _ -> (S.empty, TyCon Int)
| Bool _ -> (S.empty, TyCon Bool)
let specialize (TypeScheme{tyvars; tau}) = | String _ -> (S.empty, TyCon String)
let bindings = in
List.map (fun tv -> snd (w TE.empty exp)
(tv, new_tyvar())) (SS.elements tyvars) in
23
The workhorse function is infer_type which contains the recursive function w corresponding to
the pseudocode we presented in Section 3.1.2. The other functions are dedicated to unification
as defined in Section 3.1.1, the Clos operation, the tyvars map (finding free type variables etc.),
and instantiation as defined in Section 2.2.2.
The code and files presented so far are the ones that cover the implementation of W, however,
practicalities such as the examples we used to test our implementation are not crucial for our
thesis, but can be found in Appendix E. The whole implementation i.e. all the files used for
implementing W can be found on our GitHub page here.
24
Chapter 4
Optimizing Algorithm W
4.1.2 Unification
When using substitutions, our Unify function from 3.1.1 would either fail or return a substitution
that unifies the two types. With Union-Find, we do not have substitutions, so instead we link
the types as needed. Let Unifyopt : (Type × Type) → Substitution be the unification function
for Wopt and let () be the unit type defined in OCaml. The possible operations for Unifyopt are
defined below
Unifyopt (π1 , π2 )
Unifyopt (α1 , α2 )
Unifyopt (α, τ )
Unifyopt (τ, α)
25
Unifyopt (τ11 → τ12 , τ21 → τ22 )
Unifyopt (τ11 × τ12 , τ21 × τ22 )
Example 14 (Unification). Let the Unifyopt function take the following two types as parameter
τ1 = int → α and τ2 = int → (β × γ) then Unifyopt (τ1 , τ2 ) will do the Link(α, β × γ) operation
i.e. putting α into the set with β × γ so we get {α, β × γ} and whenever we do the Find(α)
operation it returns β × γ since it is now the representative of that set.
4.1.3 Pseudocode
We will again use the prime symbol to specify the find action on a type, i.e. τ ′ means Find(τ ).
W(Γ, e) = case e of
i =⇒ (ID, int) let x = e1 in e2 =⇒
b =⇒ (ID, bool) τ1 = W(Γ, e1 )
s =⇒ (ID, string) τ2 = W(Γ ± {x 7→ ClosΓ τ1 }, e2 )
x =⇒ in τ2
if x ∈
/ Dom Γ then fail (e1 , e2 ) =⇒
else let ∀α1 . . . αn .τ = Γ(x) τ1 = W(Γ, e1 )
β1 . . . βn be new τ2 = W(Γ, e2 )
in {αi 7→ βi }τ τ1 × τ2
λx.e1 =⇒ fst e1 =⇒
let α be a new type variable τ1 = W(Γ, e1 )
τ1 = W(Γ ± {x 7→ α}, e1 ) if typeof(τ1 ) = τ2 × τ3
in α′ → τ1 then τ2
e1 e2 =⇒ else fail
τ1 = W(Γ, e1 ) snd e1 =⇒
τ2 = W(Γ, e2 ) τ1 = W(Γ, e1 )
let α be a new type variable if typeof(τ1 ) = τ2 × τ3
Unify(τ1′ , τ2 → α) then τ3
′
in α else fail
26
4.2 Implementation
The files ast.ml and typeEnv.ml have not been changed, but substitution.ml has obviously
been deleted. In the following, we present the files that have been changed and we only write out
the parts and/or functions that have been changed substantially.
We decided to implement the Union-Find data structure directly into the types instead of
wrapping all types in nodes since we know that only tyvars can be linked to another type and
this way we will not have to unwrap the types all the time.
Note that we have implemented path compression in the Find operation. Path compression
makes sure that the pointer always points to the root instead of an intermediary.
types.ml
...
type typ =
...
| TyVar of tyvar ref
...
(* Union-find *)
let rec find typ: typ = match typ with
| TyVar ({contents = Link t} as kind) ->
let root = find t in
kind := Link root;
root
| _ -> typ
A lot has changed since we use pointers, no substitution maps and a different approach to
unification
algorithmW.ml
open Types
open Utils
module A = Ast
27
...
28
match tau1 with TyTuple { t1; _ } -> t1 | _ -> raise (Fail "expected tuple"))
| A.Snd e1 -> (
let tau1 = w gamma e1 in
match tau1 with TyTuple { t2; _ } -> t2 | _ -> raise (Fail "expected tuple"))
| A.BasVal b ->
match b with
| Int _ -> TyCon Int
| Bool _ -> TyCon Bool
| String _ -> TyCon String
in
w TE.empty exp
The examples used for testing are the same examples used in W and there is a small change in
the Pretty Printer. The examples and the Pretty Printer can be found in Appendix E. The full
implementation of Wopt can be found on our GitHub page here.
29
Chapter 5
Conclusion
In this thesis, we have described the grammar of the Hindley-Milner type system and extended
this to include integers, booleans, strings, tuples, fst, and snd. We have defined dynamic and static
semantics for the language and introduced concepts and operations such as type environments
and substitutions to help us formulate and implement type inference for the language in practice.
Formal definitions, as well as examples, have been presented for the described concepts and
operations, and we have proven consistency between our dynamic and static semantics. Unification
was introduced and its function in our algorithm, as well as its operations, were described. After
this, we presented pseudocode for a non-optimized algorithm for type inference in our language
which we call algorithm W. The soundness of algorithm W, was proven for all types, and we
presented our working implementation of the algorithm with explanations as needed.
After having a working algorithm, we looked into optimizing it by using a Union-Find data
structure instead of substitutions. We introduced the Union-Find data structure shortly along
with its three operations, namely the creation of new sets, linking two nodes, and finding the
representative node in a set. We then related this to types in our language and presented
pseudocode for the new, optimized algorithm which we denoted Wopt . After presenting the
pseudocode for the optimized algorithm, we adapted our implementation to use a Union-Find
data structure with path compression and presented the main differences in our new working
implementation. Lastly, we added details, examples, and summaries to the appendices.
30
Bibliography
[1] Luis Manuel Martins Damas. Type assignment in programming languages. chapter 2, pages
74–82. 1984.
[2] Robin Milner. A theory of type polymorphism in programming. Journal of Computer and
System Sciences, 17(3):369, 1978.
[3] Peter Sestoft. Programming language concepts for software developers. page 117, 2010.
[4] Mads Tofte. Operational semantics and polymorphic type inference. chapter 2. 1988.
31
Appendices
32
Appendix A
Operations
Parameter Result
π ∅
α α
τ1 → τ2
tyvars(τ1 ) ∪ tyvars(τ2 )
τ1 × τ2
33
A.4 Unification Table for Wopt
We will use the prime symbol to specify the find action on a type, i.e. τ ′ means Find(τ ).
Parameter Result
π1 = π2 if π1 = π2 then () else Fail
α1 = α2 if α1 = α2 then () else Link(α1 , α2 )
α=τ
if α ∈ tyvars(τ ) then Fail else Link(α, τ )
τ =α
τ11 → τ12 = τ21 → τ22 ′ , τ ′ ); U nif y(τ ′ , τ ′ )
U nif y(τ11 21 12 22
τ11 × τ12 = τ21 × τ22
otherwise Fail
34
Appendix B
Generalization
35
Appendix C
With all the details in place we can now present some type inference examples when given an
expression e.
C.1 Monomorphic
e = λx.(λy.xy)1
We start off with the S-Lambda rule
Γ ± {x 7→ α → β} ⊢ (λy.xy)1 : γ
Γ ⊢ λx.(λy.xy)1 : (α → β) → γ
note that we have to guess the type of x for later use. Furthermore, by the S-App rule we
have
Γ ⊢ λy.xy : δ → γ Γ⊢1:δ
Γ ± {x 7→ α → β} ⊢ (λy.xy)1 : γ
Γ ⊢ λx.(λy.xy)1 : (α → β) → γ
and by the S-Int rule we find out that 1 has type int and therefore we replace each occurence of
δ with int, thus we have
Γ ⊢ λy.xy : int → γ
Γ ⊢ 1 : int
Γ ± {x 7→ α → β} ⊢ (λy.xy)1 : γ
Γ ⊢ λx.(λy.xy)1 : (α → β) → γ
Γ ± {y 7→ int} ⊢ xy : γ
Γ ⊢ λy.xy : int → γ Γ ⊢ 1 : int
Γ ± {x 7→ α → β} ⊢ (λy.xy)1 : γ
Γ ⊢ λx.(λy.xy)1 : (α → β) → γ
36
by the S-App rule we get
Γ⊢x:ϵ→γ Γ⊢y:ϵ
Γ ± {y 7→ int} ⊢ xy : γ
Γ ⊢ λy.xy : int → γ Γ ⊢ 1 : int
Γ ± {x 7→ α → β} ⊢ (λy.xy)1 : γ
Γ ⊢ λx.(λy.xy)1 : (α → β) → γ
when evaluating Γ(x) > ϵ → γ and Γ(y) > ϵ our Γ = {x 7→ α → β, y 7→ int}, thus those two
operations gives
Γ(x) > ϵ → γ =⇒
ϵ=α γ=β
Γ(y) > ϵ =⇒ ϵ = int
By replacing the type variables accordingly, the resulting derivation tree looks as follows
C.2 Polymorphic
e = let id = λx.x in (id 1, id ”hello”)
By the S-Let rule we have
37
Γ ± {x 7→ γ} ⊢ x : δ
Γ ± {id 7→ ClosΓ β} ⊢ (id 1, id ”hello”) : α
Γ ⊢ λx.x : β
Γ ⊢ let id = λx.x in (id 1, id ”hello”) : α
Γ ± {x 7→ γ} ⊢ x : δ
Γ ± {id 7→ ClosΓ γ → δ} ⊢ (id 1, id ”hello”) : α
Γ ⊢ λx.x : γ → δ
Γ ⊢ let id = λx.x in (id 1, id ”hello”) : α
now, we have to calculate the ClosΓ γ → γ operation to evaluate the tuple, thus we have
38
x ∈ Dom Γ1 Γ1 (x) > γ
Γ ± {x 7→ γ} ⊢ x : γ Γ2 ⊢ id 1 : ϵ Γ2 ⊢ id ”hello” : ζ
Γ ⊢ λx.x : γ → γ Γ ± {id 7→ ∀γ.γ → γ} ⊢ (id 1, id ”hello”) : ϵ × ζ
Γ ⊢ let id = λx.x in (id 1, id ”hello”) : ϵ × ζ
by the S-Lookup, S-Int, and S-String rule and the replacement of η = int and θ = string we
get
39
id ∈ Dom Γ2 Γ2 (id) > int → ϵ id ∈ Dom Γ2 Γ2 (id) > string → ζ
x ∈ Dom Γ1 Γ1 (x) > γ Γ2 ⊢ id : int → ϵ Γ2 ⊢ 1 : int Γ2 ⊢ id : string → ζ Γ2 ⊢ ”hello” : string
Γ ± {x 7→ γ} ⊢ x : γ Γ2 ⊢ id 1 : ϵ Γ2 ⊢ id ”hello” : ζ
Γ ⊢ λx.x : γ → γ Γ ± {id 7→ ∀γ.γ → γ} ⊢ (id 1, id ”hello”) : ϵ × ζ
Γ ⊢ let id = λx.x in (id 1, id ”hello”) : ϵ × ζ
40
we now do the instantiations
thus, ϵ = int and ζ = string and the resulting derivation tree looks like
41
id ∈ Dom Γ2 Γ2 (id) > int → int id ∈ Dom Γ2 Γ2 (id) > string → string
x ∈ Dom Γ1 Γ1 (x) > γ Γ2 ⊢ id : int → int Γ2 ⊢ 1 : int Γ2 ⊢ id : string → string Γ2 ⊢ ”hello” : string
Γ ± {x 7→ γ} ⊢ x : γ Γ2 ⊢ id 1 : int Γ2 ⊢ id ”hello” : string
Γ ⊢ λx.x : γ → γ Γ ± {id 7→ ∀γ.γ → γ} ⊢ (id 1, id ”hello”) : int × string
Γ ⊢ let id = λx.x in (id 1, id ”hello”) : int × string
42
The expression e thus have the type int × string.
43
Appendix D
Additional Proofs
S-Snd
Γ ⊢ (e1 , e2 ) : τ1 × τ2
(D.1)
Γ ⊢ snd(e1 , e2 ) : τ1
D-Snd
E ⊢ e1 → v1 E ⊢ e 2 → v2
(D.2)
E ⊢ snd(e1 , e2 ) → v1
D-Snd-Wrong1
E ⊢ e1 → wrong E ⊢ e2 → v2
(D.3)
E ⊢ snd(e1 , e2 ) → wrong
D-Snd-Wrong2
E ⊢ e 1 → v1 E ⊢ e2 → wrong
(D.4)
E ⊢ snd(e1 , e2 ) → wrong
We see that e1 → r1 and e2 → r2 for some r1 , r2 .
By our I.H. and 2.1, D.1, (D.2 / D.3 / D.4) we get
r1 ̸= wrong (D.5)
⊨ r1 : τ1 (D.6)
This means the evaluation cannot have been D.3
By our I.H. and 2.1, D.1, (D.2 / D.4) we get
r2 ̸= wrong (D.7)
⊨ r2 : τ2 (D.8)
This means the evaluation cannot have been D.4. The evaluation must have been D.2, meaning
that r1 = v1 , r2 = v2 , and r = v2 ∈ Val. Since a Val cannot be wrong then r ̸= wrong.
From D.8 we have that ⊨ r2 : τ2 . Since r = v2 = r2 and ⊨ r2 : τ2 , we must have that ⊨ r : τ2 .
44
D.2 Soundness Proof for snd(e1 , e2 )
We want to show S1 Γ ⊢ snd (e1 , e2 ) : τ2
From our algorithm we have
(S1 , τ1 × τ2 ) = W(Γ, (e1 , e2 )) (D.9)
By lemma 2.2.2 where Γ = Γ and S = S1 we have
S
1
Γ −→ S1 Γ (D.10)
S1 Γ ⊢ (e1 , e2 ) : τ1 × τ2 (D.11)
45
Appendix E
46
let string_of_typescheme (TypeScheme { tyvars; tau }) =
let tyvars = String.concat ", " (List.map (fun x -> string_of_int x) (SS.elements tyvars))
in "forall " ^ tyvars ^ " . " ^ (string_of_tau tau)
let print_typescheme typescheme = print_string (string_of_typescheme typescheme ^ "\n")
47
let elems = String.concat ", " (List.map (fun x -> string_of_int x) tyvars) in
"{ " ^ elems ^ " }"
let print_tyvars tyvars = print_string (string_of_tyvars tyvars ^ "\n")
48