Programming Language Technology, DAT151/DIT231
Lambda calculus with integers:
e,f ::= x -- variable
| λx → e -- function abstraction
| f e -- application
| let x = e₁ in e₂ -- local binding
| n -- integer literal
| e₁ op e₂ -- arithmetic operationGrammar for types:
t,s ::= int -- base type of integers
| s → t -- function typeArrow associates to the right, so
r → s → t = r → (s → t)
Higher order functions:
twice : (int → int) → (int → int)
twice = λ f → λ x → f (f x)
Order of a function type:
ord(int) = 0
ord(s → t) = max { ord(s) + 1, ord(t) }
Examples:
1st order function: Takes base values as inputs.
int → int
int → int → int
ord(int → (int → int))
= max { ord(int)+1, ord(int → int) }
= max { ord(int)+1, ord(int)+1, ord(int) }
= max {1,1,0} = 12nd order function: Accepts even 1st order functions.
(int → int) → int
(int → int) → int → int
(int → int) → (int → int) → int
(int → int → int) → int → int3rd order function: Has a 2nd order function as argument.
((int → int) → int) → intQuiz (menti.com 84 744 66): What is the order of this type?
(int → int) → ((int → int) → int) → int
Judgement: Γ ⊢ e : t.
Context Γ maps variables to types.
Variable:
--------- Γ(x) = t
Γ ⊢ x : tAbstraction:
Γ,x:s ⊢ e : t
-----------------------
Γ ⊢ (λ x → e) : (s → t)Application:
Γ ⊢ f : s → t
Γ ⊢ e : s
-------------
Γ ⊢ f e : tLet:
Γ ⊢ e₁ : s
Γ,x:s ⊢ e₂ : t
--------------------------
Γ ⊢ (let x = e₁ in e₂) : tDesugaring of let:
Γ,x:s ⊢ e₂ : t
---------------------
Γ ⊢ (λx → e₂) : s → t Γ ⊢ e₁ : s
-----------------------------------
Γ ⊢ (λx → e₂) e₁ : t
Example: Typing of twice.
Deriving
⊢ λ f → λ x → f (f x) : (int → int) → int → int.
----------------- --------------
... ⊢ f : int→int ... ⊢ x : int
-------------------------------- -----------------------------------
f:int→int, x:int ⊢ f : int → int f:int→int, x:int ⊢ f x : int
--------------------------------------------------------------------
f:int→int, x:int ⊢ f (f x) : int
---------------------------------------
f:int→int ⊢ λ x → f (f x) : int → int
-----------------------------------------------
⊢ λ f → λ x → f (f x) : (int → int) → int → int
(Naive) type checking works well for abstraction:
void check(Cxt Γ, Exp e, Type t)
check(Γ, λx→e, int): type error
check(Γ, λx→e, s → t):
check (Γ[x:s], e, t)
... but gets stuck at application:
check(Γ, f e, t):
let s = ???
check(Γ, f, s→t)
check(Γ, e, s)
Type inference works well for application:
Type infer(Cxt Γ, Exp e)
infer(Γ, f e):
case infer(Γ,f) of
int: type error
(s → t): if s == infer(Γ,e)
then return t
else type error
... but gets stuck at abstraction:
infer(Γ, λx→e):
let s = ???
let t = infer(Γ[x:s], e)
return (s → t)
Solutions:
Bidirectional type-checking:
check lambdas,
infer applications: infer function part, check argument part
void check(Cxt Γ, Exp e, Type t)
Type infer(Cxt Γ, Exp e)
check(Γ, λx→e, int ): type error
check(Γ, λx→e, s → t): check (Γ[x:s], e, t)
check(Γ, e , t ): unless (infer(Γ,e) == t) type error
infer(Γ, x) : lookup(Γ,x)
infer(Γ, f e):
case infer(Γ,f) of
int: type error
(s → t): check(Γ,e,s)
return tlet x = e₁ in e₂:
e₁ needs to be inferrede₂ can be checked or inferred(λ x → f) eInference with type-annotated binders:
λ(x:s) → e and let x:s = e₁ in e₂
infer(Γ, λ(x:s)→e):
let t = infer(Γ[x:s], e)
return (s → t) C x = new C(e); let x:int = 3 in x + xInference with type variables:
???s as variables for typesExample: Inferring the comp function.
(A, B, C etc. are type
variables.)
⊢ λ f → λ g → λ x → f (g x) : A
A = B → C
f:B ⊢ λ g → λ x → f (g x) : C
C = D → E
f:B, g:D ⊢ λ x → f (g x) : E
E = F → G
f:B, g:D, x:F ⊢ f (g x) : G
- f:B, g:D, x:F ⊢ f : H → G
B = H → G
- f:B, g:D, x:F ⊢ g x : H
* f:B, g:D, x:F ⊢ g : I → H
D = I → H
* f:B, g:D, x:F ⊢ x : I
F = I
Constraint solving steps (solved constraints above the bar):
A = B → C
C = D → E
E = F → G
B = H → G
D = I → H
F = I
A = B → C
---------
C = D → E
E = F → G
B = H → G
D = I → H
F = I
A = B → (D → E)
C = D → E
---------
E = F → G
B = H → G
D = I → H
F = I
A = B → (D → (F → G))
C = D → (F → G)
E = F → G
---------
B = H → G
D = I → H
F = I
A = (H → G) → (D → (F → G))
C = D → (F → G)
E = F → G
B = H → G
---------
D = I → H
F = I
A = (H → G) → ((I → H) → (F → G))
C = (I → H) → (F → G)
E = F → G
B = H → G
D = I → H
---------
F = I
A = (H → G) → ((I → H) → (I → G))
C = (I → H) → (I → G)
E = I → G
B = H → G
D = I → H
F = I
---------
Solution
⊢ λ f → λ g → λ x → f (g x) : (H → G) → ((I → H) → (I → G))
Generalization
⊢ λ f → λ g → λ x → f (g x) : ∀ α β γ. (β → γ) → ((α → β) → (α → γ))
Extended type grammar:
s,t ::= ...
| X -- type variable
A type that does not mention any type variable is called closed.
A substitution σ maps type variables
X to types t.
The application of a substitution σ to a type
t is written tσ.
X σ = σ(X)
int σ = int
(s → t) σ = sσ → tσ
A substitution σ is usually given by a finite list of
mappings X₁=s₁,...Xₙ=sₙ.
Then σ(Xᵢ) = sᵢ, and σ(Y) = Y if
Y ∉ {X₁,...Xₙ}.
Example:
(X → U)[X = Y → int] = (Y → int) → U
The application of a substitution σ to another
substitution τ is written τσ.
This is also called the composition of substitutions and
written σ ∘ τ (see book).
t(τσ) = (tτ)σ
So:
(τσ)(X) = (τ(X))σ
Example for substitution composition:
[X=Y→int][Y=int] = [X=int→int,Y=int]
X[X=Y→int][Y=int] = (Y→int)[Y=int] = int → int
Y[X=Y→int][Y=int] = Y[Y=int] = int
Z[X=Y→int][Y=int] = Z[Y=int] = Z
State of the type inference:
Potentially infinite supply of type variables (e.g. X₀, X₁,
...).
Allows allocation of new type variable.
Type newTypeVariableStore for equality constraints.
A constraint s ≐ t is a pair of types s and
t that need to be unified.
void equal (Type s, Type t)
adds the new constraint s ≐ t to the store.
Inference phase: Build up store of constraints.
infer(Γ, x):
return lookup(Γ,x)
infer(Γ, λx→e):
s ← newTypeVariable
t ← infer(Γ[x:s], e)
return (s → t)
infer(Γ, f e):
r ← infer(Γ, f)
s ← infer(Γ, e)
t ← newTypeVariable
equal(r,s→t)
return t
Optimization of application case:
infer(Γ, f e):
r ← infer(Γ, f)
case r of
int : type error
(s → t):
s' ← infer(Γ, e)
equal(s,s')
return t
X: s ← infer(Γ, e)
t ← newTypeVariable
equal(X,s→t)
return t
Try to find a substitution σ from type variables to types that solves all constraints.
State of solver:
Constraint store: Additional methods:
Bool solved() -- is the store empty?
Constraint takeConstraint() -- extract a constraint, removing it from the storeA substitution.
Invariant: The substitution is already applied to the state (constraints
and itself).
void solveVar(TypeVariable X, Type t)
solveVar applies substitution [X=t] to the
state (all constraints and the substitution).
Algorithm:
while (not solved()) {
(s ≐ t) ← takeConstraint()
unify(s,t)
}
Unification:
unify(X,X)
unify(int, int):
-- do nothing
unify(s₁→t₁, s₂→t₂):
equal(s₁,s₂)
equal(t₁,t₂)
unify(int,s→t)
unify(s→t,int):
type error
unify(X,t)
unify(t,X):
if X occurs in t then type error
else solveVar(X,t)
Example 1: Compute type of twice
Inference phase
infer(ε, λ f → λ x → f (f x)) ... = F → (X → Z)
infer(f:F, λ x → f (f x)) ... = X → Z
infer(f:F,x:X, f (f x)) ... = Z
- infer(..., f) = F
- infer(..., f x) ... = Y
* infer (..., f) = F
* infer (..., x) = X
* F = X → Y
* result: Y
- F = Y → Z
- result: ZSolving phase (substitution on top, constraints on bottom)
-------------
F = X → Y
F = Y → Z
F = X → Y
-------------
X → Y = Y → Z
F = X → Y
-------------
X = Y
Y = Z
F = Y → Y
X = Y
-------------
Y = Z
Final substitution:
F = Z → Z
X = Z
Y = Z
-------------Applied to computed type F → (X → Z) gives
(Z → Z) → (Z → Z).
Example 2: Compute type of λ x → x x
infer(ε, λ x → x x) = X → Y
infer(x:X, x x) = Y
X = X → Y
result: Y
The constraint X = X → Y is unsolvable as it violates
the occurs check.
Some variables may be left unconstrained, e.g.:
comp : (Y → Z) → (X → Y) → (X → Z)
comp should have any instance of this
type:
Any instantiation of X, Y, Z
gives a valid type.
comp has polymorphic type:
comp : ∀ α β γ. (β → γ) → (α → β) → α → γ
Extension of type-inference (sketch):
Extend the grammar by universal variables α
(X are existential variables).
A type scheme is of the form
∀ α₁...αₙ. t.
Context Γ maps variables x to
schemes.
Instantiation:
When we lookup a scheme in the context, Γ(x),
instantiate the scheme to fresh existential variables
X₁...Xₙ.
Generalization:
When the type t of expression e₁ in
let x = e₁ in e₂ has unconstrained existential variables
X₁...Xₙ that do not appear in the current context
Γ,
replace them by universal variables α₁...αₙ and assign
x the scheme
∀α₁...αₙ.t[α₁...αₙ/X₁...Xₙ].
Example:
twice : ∀α. (α → α) → α → α ⊢ twice twice (λ x → x) : Y
- ... ⊢ twice twice : Y → Y
* ... ⊢ twice : (X → X) → (X → X)
* ... ⊢ twice : (Y → Y) → (Y → Y)
* X → X = (Y → Y) → (Y → Y)
- ... ⊢ λ x → x : Z → Z
Y = Z → Z
twice : ∀α. (α → α) → α → α ⊢ twice twice (λ x → x) : ∀ β. β → β