11-TypeInfer
11-TypeInfer
Yuepeng Wang
Spring 2025
1
Overview
• Review of type checking
• Constraint-based typing
• Unification
• Type inference
• Do not ask the programmer for type annotations
• Automatically find an appropriate type of every expression in the
program and report errors if types are incompatible
3
Motivation of Type Inference
• Providing type annotations is good because it facilitates type checking
• Haskell: ((Int -> Int) -> (Int -> Int)) -> (Int -> Int) -> (Int -> Int)
• C++: std::vector<std::vector<int>>::iterator
• Type inference can help programmers get benefits from type checking
without writing complicated type annotations
4
Review of Type Checking in FUN
• Consider the FUN language with type annotations
5
Type Inference Example 1
• Now remove the type annotations and consider the FUN program:
let x = 1 in x
• It has a variable. But do we really need type annotations to know the type
of this expression?
6
Type Inference Example 2
• Consider the following FUN program:
let f = lambda x. x + 1 in f
7
Type Inference Example 3
• Consider the following FUN program:
let f = lambda x. lambda y. x + y in f
8
Type Inference Example 4
• Consider the following FUN program:
let f = lambda x. lambda y. x + 1 in f
• We know function f adds one to its first argument
9
Type Inference Example 5
• Consider the following FUN program:
let f = lambda g. app g 1 in f
• From "app g 1", the type of g is Int→ X, and the type of "app g 1" is X
• From "app g (app g 1)", the type of g must be X → Y
12
High-level Idea of Type Inference
• Type inference on "lambda x. 1 + x"
• Perform type checking for program "lambda x: X. 1 + x" and verify its
type is X → Y
13
Example of Type Inference
• Type inference on "lambda x. 1 + x", conceptually
14
High-level Idea of Type Inference
• This idea also works for other programs!
15
Hindley-Milner Type Inference
• The Hindley-Milner (H-M) type inference uses this idea
• Roger Hindley
16
The FUN Language
• Consider the FUN language without type annotations
17
Types in the FUN Language
• Types of expressions in the FUN language are defined as follows
• Constraint-based typing
• Constraint solving
19
Judgments for Constraint-Based Typing
• Judgments for constraint-based typing are of the form
Γ⊢e:T|C
• For integer constant c, its type is Int and the typing constraints are empty
• For boolean constant b, its type is Bool and the typing constraints are empty
• For identifier (variable) x, its type is determined by the typing environment Γ,
which may get stuck if there is no entry for x in Γ, i.e. x ∉ dom(Γ). The
typing constraints are also empty
21
Alternative Rules for Identifiers
e ::= c | b | x | '(' e ')'
| e '+' e | e '-' e | ... Ident x x ∈ dom(Γ) Γ(x) = T
(CT-Ident1)
| e '==' e | ... Γ ⊢ x : T | {}
| 'if' e 'then' e 'else' e
| 'lambda' x '.' e
Ident x x ∉ dom(Γ)
| 'app' e e (CT-Ident2)
| 'let' x '=' e 'in' e Γ ⊢ x : Terr | {Cerr}
• There is a way to make the constraint-based typing rules never get stuck
• We can always check if the identifier x ∈ dom(Γ). If yes, the type is Γ(x)
• If not, we generate a constraint that is always unsatisfiable (e.g., a special
constraint Cerr or Int = Bool). The type error is deferred to be revealed
until the constraint solving phase
22
The Rule for Plus
e ::= c | b | x | '(' e ')'
| e '+' e | e '-' e | ... Γ ⊢ e1 : T1 | C1
| e '==' e | ...
Γ ⊢ e2 : T2 | C2
| 'if' e 'then' e 'else' e
| 'lambda' x '.' e C = C1 ∪ C2 ∪ {T1 = Int, T2 = Int}
(CT-Plus)
| 'app' e e
Γ ⊢ e1 + e2 : Int | C
| 'let' x '=' e 'in' e
• In the FUN language, the + operator only works for integers, so we know
the result of e1 + e2 must be of type Int
23
The Rule for Minus
e ::= c | b | x | '(' e ')'
| e '+' e | e '-' e | ... Γ ⊢ e1 : T1 | C1
| e '==' e | ...
Γ ⊢ e2 : T2 | C2
| 'if' e 'then' e 'else' e
| 'lambda' x '.' e C = C1 ∪ C2 ∪ {T1 = Int, T2 = Int}
(CT-Minus)
| 'app' e e
Γ ⊢ e1 − e2 : Int | C
| 'let' x '=' e 'in' e
24
The Rule for Equal
e ::= c | b | x | '(' e ')'
| e '+' e | e '-' e | ... Γ ⊢ e1 : T1 | C1
| e '==' e | ...
Γ ⊢ e2 : T2 | C2
| 'if' e 'then' e 'else' e
| 'lambda' x '.' e C = C1 ∪ C2 ∪ {T1 = T2}
(CT-Eq)
| 'app' e e
Γ ⊢ e1 == e2 : Bool | C
| 'let' x '=' e 'in' e
• Unlike T-Eq for type checking, CT-Eq does not restrict the operands of ==
to be only primitive types. It also works for function types
• Otherwise, there would be disjunctions (logical or) in the constraints,
e.g., T1 = Int ∨ T1 = Bool, which makes constraint solving harder
Int 1 Int 2
(CT-Int) (CT-Int)
Int 3 Γ ⊢ 1 : Int | {} Γ ⊢ 2 : Int | {}
(CT-Int) (CT-Plus)
Γ ⊢ 3 : Int | {} Γ ⊢ 1 + 2 : Int | {Int = Int}
(CT-Eq)
Γ ⊢ 3 == 1 + 2 : Bool | {Int = Int}
26
The Rule for If-Then-Else
e ::= c | b | x | '(' e ')'
Γ ⊢ e1 : T1 | C1
| e '+' e | e '-' e | ...
| e '==' e | ...
Γ ⊢ e2 : T2 | C2
| 'if' e 'then' e 'else' e Γ ⊢ e3 : T3 | C3
| 'lambda' x '.' e
C = C1 ∪ C2 ∪ C3 ∪ {T1 = Bool, T2 = T3}
| 'app' e e (CT-ITE)
| 'let' x '=' e 'in' e
Γ ⊢ if e1 then e2 else e3 : T2 | C
• The type of the if-then-else expression is the same as the type of both
branches
27
The Rule for Function Abstractions
e ::= c | b | x | '(' e ')'
| e '+' e | e '-' e | ...
| e '==' e | ...
fresh X
| 'if' e 'then' e 'else' e Γ[x ◃ X] ⊢ e : T | C
(CT-Abs)
| 'lambda' x '.' e
| 'app' e e
Γ ⊢ lambda x . e : X → T | C
| 'let' x '=' e 'in' e
30
The Rule for Function Applications
e ::= c | b | x | '(' e ')' fresh X1, X2
| e '+' e | e '-' e | ...
Γ ⊢ e1 : T1 | C1
| e '==' e | ...
| 'if' e 'then' e 'else' e Γ ⊢ e2 : T2 | C2
| 'lambda' x '.' e
C = C1 ∪ C2 ∪ {T1 = X1 → X2, T2 = X1}
| 'app' e e (CT-App)
| 'let' x '=' e 'in' e Γ ⊢ app e1 e2 : X2 | C
31
The Rule for Let Bindings
e ::= c | b | x | '(' e ')' fresh X
| e '+' e | e '-' e | ... Γ[x ◃ X] ⊢ e1 : T1 | C1
| e '==' e | ...
| 'if' e 'then' e 'else' e Γ[x ◃ X] ⊢ e2 : T2 | C2
| 'lambda' x '.' e C = C1 ∪ C2 ∪ {X = T1}
| 'app' e e (CT-Let)
| 'let' x '=' e 'in' e Γ ⊢ let x = e1 in e2 : T2 | C
33
Example of Constraint-Based Typing
• Constraint-based typing for "let x = True in 2 + x"
34
fi
Constraint Solving
• After obtaining the set of constraints from the typing rules, we need to
solve the constraints and get types for the type variables
• A solution (called a unifier) to a set of type constraints is a substitution σ
that maps type variables to types (concrete types or type variables) such
that all type constraints are satisfied
• For each equality constraint, the LHS and RHS must be syntactically
identical (i.e., have identical tree representations) after applying σ
36
Composition of Substitutions
• A substitution σ1 can be composed with another substitution σ2, denoted
by σ1 ∘ σ2 to a form a new substitution
σ1 ∘ σ2 = [ ]
X ↦ Tσ1 for each (X ↦ T) ∈ σ2 and
X↦T for each (X ↦ T) ∈ σ1 with X ∉ dom(σ2)
• σ1 ∘ σ2 contains
• (1) a mapping X ↦ Tσ1 for each X ↦ T in σ2
• (2) all mappings X ↦ T in σ1 where X is not in the domain of σ2
• Property: T(σ1 ∘ σ2) = (Tσ2)σ1, i.e. applying σ1 ∘ σ2 to T is the same as
first applying σ2 to T and then applying σ1 to the result
37
Example of Composition
• Given σ1 = [X0 ↦ Bool → X1, X2 ↦ Int] and σ2 = [X1 ↦ X2], what is
σ1 ∘ σ2 ?
38
Generality of Unifiers
• Some unifier is more general than others. We say σ2 is more general than (at
least as general as) σ1 if there exists a substitution σ such that σ1 = σ ∘ σ2
39
Unification
• The most general unifier of type constraints can be found by unification
procedure unify(C)
The unifier is an empty map if there is no constraint
if C = {} then [ ]
else assert C = {S = T} ∪ C′ Take one constraint in the form of S = T from all constraints
if S and T are identical
then unify(C′) "Occur check" to avoid generating a cyclic substitution like
else if S is variable X and X ∉ TVars(T ) X ↦ X → X. TVars(T ) returns all type variables in T
then unify(C′[X ↦ T]) ∘ [X ↦ T]
else if T is variable X and X ∉ TVars(S)
C′[X ↦ T] means apply the substitution [X ↦ T] to the
then unify(C′[X ↦ S]) ∘ [X ↦ S]
remaining constraints C′
else if S is S1 → S2 and T is T1 → T2
then unify(C′ ∪ {S1 = T1, S2 = T2})
If both S and T are function types, we add equalities for
else
their input types S1 = T1 and return types S2 = T2 into the
fail
constraints
40







Example of Unification
• Consider constraints C = {X1 = Bool, Int → X2 = X3 → X1}
unify(C)
= unify({Int → X2 = X3 → X1}[X1 ↦ Bool]) ∘ [X1 ↦ Bool]
= unify({Int → X2 = X3 → Bool}) ∘ [X1 ↦ Bool]
= unify({Int = X3, X2 = Bool}) ∘ [X1 ↦ Bool]
= (unify({X2 = Bool}[X3 ↦ Int]) ∘ [X3 ↦ Int]) ∘ [X1 ↦ Bool]
= (unify({X2 = Bool}) ∘ [X3 ↦ Int]) ∘ [X1 ↦ Bool]
=…
= ([X2 ↦ Bool] ∘ [X3 ↦ Int]) ∘ [X1 ↦ Bool]
= [X2 ↦ Bool, X3 ↦ Int] ∘ [X1 ↦ Bool]
= [X2 ↦ Bool, X3 ↦ Int, X1 ↦ Bool]
41
Type Inference Result
• Given an expression e, perform constraint-based typing Γ ⊢e:T|C
with Γ being an empty map
• If constraints C are satisfied with a most general unifier σ, then the type of
expression e is Tσ
42
Fresh in Haskell
• We can use the State monad ("State s" monad) to implement fresh
functions in Haskell
43
The State Monad
• State is a type constructor of kind * -> * -> *
• "State s a" is a type where s is the type of the internal state and a is
the type of the result
• "State s" is a monad
• Common functions
(>>=) :: State s a -> (a -> State s b) -> State s b
return :: a -> State s a
get :: State s s -- sets the result to be the state
put :: s -> State s () -- sets the result to (), sets the state
evalState :: State s a -> s -> a -- takes a state monad and the initial state
-- and returns the final result
runState :: State s a -> s -> (a, s) -- similar to evalState, also returns next state
44
The State Monad
• Suppose we use Int as the internal state
type InferState a = State Int a
• InferState is a monad
45
Fresh Functions
• Implement getFreshInt
getFreshInt :: InferState Int
getFreshInt = do n <- get
put (n + 1)
return n
46