CS 565 October 6, 1997 Jens Palsberg Readings: Nothing else.

Lecture Note 7:

Simple Types for the Core of Scheme

Contents

1 Introduction 1

2 Simple Types 2

3 Constraints 4

4 First-Order Unification 7

1 Introduction

Specification is about What, and implementation is about How. When dealing with a piece of data, it is sufficient to know the specification. The specification is a contract between the user and the implementer. The implementer promises to implement at least what the specification says, and the user promises to do at most what is allowed by the specification. For example, suppose that a specification of a piece of data says:

The data f is a function which maps integers to integers.

Notice that the specification only promises that if we apply f to an integer, then we will get an integer back. Thus, the contract is clear: the implementer is done with the work when f can map integers to integers, and the user can then use f to do exactly that. A type is a simple form of specification. This note presents a type system for the λ-calculus, and a way of doing type inference, based on solving type constraints by first-order unification.

2 Simple Types

Our example language is a λ-calculus where the only two kinds of data are functions and integers. The language is generated by the following grammar:

E ::= x | λx.E | E_{1}E_{2} | 0 | succ E

We will use the standard convention that when writing λx.e, then e is everything until the next “)” (or until the end of the whole λ term). The slogan is, “the body of a λ extends as long as possible.” We need two kinds of type: function types and an integer type. Types are generated from the grammar:

t ::= t_{1} → t_{2} | Int

Such types are called simple types. The basic idea is that we can assign natural types to data, for example:

0 : Int
λx.(succ x) : Int → Int .

Notice that there are infinitely many types. Notice also that each type can be written as a tree: the syntax tree of the type. Let us now consider a formal system for assigning types to λ-terms. If E is a λ-term, t is a type, and A is a type environment, i.e., a partial function assigning types to variables, then the judgment

A ⊢ E : t

means that E has the type t in the environment A. Formally, this holds when the judgment is derivable using the following five rules:

A ⊢ 0 : Int  (1)
\frac{A ⊢ E : Int}{A ⊢ succ E : Int}  (2)
A ⊢ x : t (provided A(x) = t)  (3)
\frac{A[x ← s] ⊢ E : t}{A ⊢ λx.E : s → t}  (4)

\frac{A ⊢ E : s → t A ⊢ F : s}{A ⊢ EF : t}  (5)

Notice that there is one rule for each of the five constructs in the language. The hypotheses, if any, are written above the line, and the conclusion is written below the line. Two of the rules have no hypotheses so we call them axioms. We can now use these rules to derive the typings of 0 and λx.(succ x) from above. The first case is easy:

∅ ⊢ 0 : Int

The judgment is indeed an instance of the axiom for 0. The second case requires the use of the rules for abstraction, succ, and variable:

\frac{\frac{\{x : Int\} ⊢ x : Int}{\{x : Int\} ⊢ succ x : Int}}{∅ ⊢ λx.succ x : Int → Int}

Here are some more examples of the use of the five rules. The identity function:

\frac{\{x : Int\} ⊢ x : Int}{∅ ⊢ λx.x : Int → Int}

The apply function:

\frac{\{f : s → t, x : s\} ⊢ f : s → t \{f : s → t, x : s\} ⊢ x : s}{\{f : s → t, x : s\} ⊢ fx : t}

{f : s → t} ⊢ λx.fx : s → t
∅ ⊢ λf.λx.fx : (s → t) → (s → t)

The K combinator:

\frac{\frac{\{x : s, y : t\} ⊢ x : s}{\{x : s\} ⊢ λy.x : t → s}}{∅ ⊢ λx.λy.x : s → (t → s)}

The following λ-term does not have a type:

λf.(f(λy.y))(f0)

The problem is that f is applied both to a function and to 0. Let us try to build a type derivation. First, λf.(f(λy.y))(f0) is an abstraction, and the corresponding rule says that it must be given a function type. Let us give it the type s → t, where we have not yet committed to particular choices of s and t. Next we have to find a type for the application (f(λy.y))(f0). The corresponding rule says that (f(λy.y)) must be given a function type. The range of that function type must be t, so let us choose r → t, where we have not yet committed to r. Let us summarize what we have so far:

\frac{\frac{\{f : s\} ⊢ f(λy.y) : r → t \{f : s\} ⊢ f0 : r}{\{f : s\} ⊢ (f(λy.y))(f0) : t}}{∅ ⊢ λf.(f(λy.y))(f0) : s → t}

Now we have to type each of the applications f(λy.y) and f0. Surely, λy.y has a type of the form u → u, so we are trying to build a derivation of the form:

\frac{\{f : s\} ⊢ f : s \{f : s\} ⊢ λy.y : u → u}{\{f : s\} ⊢ f(λy.y) : r → t}  \frac{\{f : s\} ⊢ f : s \{f : s\} ⊢ 0 : Int}{\{f : s\} ⊢ f0 : r}
{f : s} ⊢ (f(λy.y))(f0) : t
∅ ⊢ λf.(f(λy.y))(f0) : s → t

If we first look at the left part of this derivation, then we see that it only works if

s = (u → u) → (r → t) .

If we next look at the right part of the derivation, then we see that it only works if

s = Int → r .

In both cases, we get this from the rule for application. But now we have a problem, because we need

(u → u) → (r → t) = s = Int → r

and this is only possible if (u → u) = Int which is not the case. We conclude that λf.(f(λy.y))(f0) has no simple type.

3 Constraints

How can we in general decide if a λ-term has a simple type or not? The answer is: use constraints! Our strategy is to

1. generate type constraints from the program text, and

2. solve the constraints, or decide that no solution exists.

The constraints will be designed such that the program has a simple type if and only if the constraint system is satisfiable. Given a λ-term E, we now describe how to generate a certain constraint system. Assume that E has been α-converted so that all bound variables are distinct. Let X_{E} be a set of type variables consisting of one type variable ⟨x⟩ for each λ-variable x occurring in E, and let Y_{E} be a set of variables disjoint from X_{E} consisting of one variable [[F]] for each occurrence of a subterm F of E. (The notation [[F]] is ambiguous because there may be more than one occurrence of F in E. However, it will always be clear from context which occurrence is meant.) The following constraint system uses X_{E} ∪ Y_{E} as type variables:

• for every occurrence in E of a subterm of the form 0, the equality

Int = [[0]] ;

• for every occurrence in E of a subterm of the form succ F, the two equalities

Int = [[succ F]]
[[F]] = Int ;

• for every occurrence in E of a subterm of the form λx.F, the equality

⟨x⟩ → [[F]] = [[λx.F]] ;

• for every occurrence in E of a subterm of the form GH, the equality

[[G]] = [[H]] → [[GH]] ;

• for every occurrence in E of a λ-variable x, the equality

⟨x⟩ = [[x]] .

Denote by T (E) the system of constraints generated from E in this fashion. All constraints are of one of the three forms:

v = Int
v = v′
v = v′ → v′′

A set of such constraints is satisfiable if there is a mapping from type variables to types such that all constraints are satisfied. To check satisfiability, we apply first-order unification. This doable in O(n) time, where n is the size of the constraint system.

Theorem 1 For every λ-term E we have that E has a simple type if and only if T (E) is satisfiable.

We will not study the proof here, but informally it is easy to see that the theorem makes sense: for each construct in the language, the constraints directly reflect relationships imposed by the corresponding type rule. The good news is that we now can check typability by first generating constraints, and then try to solve them. Let us take a look again at the untypable λ-term λf.(f(λy.y))(f0). Let us label the two references to f as f_{1} and f_{2}. Thus we think of the term as λf.(f_{1}(λy.y))(f_{2}0), where the labels are just our way of distinguishing the two places in the program. The constraints for this λ-term are:

⟨f⟩ → [[(f_{1}(λy.y))(f_{2}0)]] = [[λf.(f_{1}(λy.y))(f_{2}0)]]
[[f_{1}(λy.y)]] = [[f0]] → [[(f_{1}(λy.y))(f_{2}0)]]
[[f_{1}]] = [[λy.y]] → [[f_{1}(λy.y)]]
[[f_{2}]] = [[0]] → [[f0]]
⟨y⟩ → [[y]] = [[λy.y]]
[[0]] = Int
⟨f⟩ = [[f_{1}]]
⟨f⟩ = [[f_{2}]]
⟨y⟩ = [[y]]

Let us then try to solve these constraints by doing some unification:

Int → [[f0]] = [[0]] → [[f0]]
= [[f_{2}]]
= ⟨f⟩
= [[f_{1}]]
= [[λy.y]] → [[f_{1}(λy.y)]]
= (⟨y⟩ → [[y]]) → [[f_{1}(λy.y)]]

Unification gives that

Int = ⟨y⟩ → [[y]]

follows from the above constraints. But this is not satisfiable! By applying Theorem 1 we then get that λf.(f(λy.y))(f0) is not typable. Further inspection reveals that the reason for the unsatisfiability is exactly the same as why we could not build a type derivation for λf.(f(λy.y))(f0) in the previous section. The advantage of using constraints is that now we know for sure that it is impossible to build a type derivation, thanks to Theorem 1.

4 First-Order Unification

In general, the problem of first-order unification is as follows. Given a constraint

s = t

where s and t are finite trees, possibly with variables at the leaves, find a substitution θ which maps variables to finite trees such that

θ(s) = θ(t) .

We say that the substitution θ unifies s and t, and that θ solves s = t, and that θ satisfies s = t. The problem can be generalized to a family of constraints. In that case, the problem is to produce one substitution which solves all of the constraints. One can solve this unification problem in O(n) time, where n is the size of the constraint set (M. S. Paterson and M. N. Wegman, “Linear Unification”, Journal of Computer and System Sciences, 16:158–167, 1978.) Here, we will not cover this linear-time algorithm. Rather we will present a simpler algorithm with worse worst-case complexity. In our setting, a tree is either a type variable, Int, or of the form s → t where s and t are trees. If we are given the constraint system

v = w → Int
w = Int → Int ,

then the substitution θ where

θ(v) = (Int → Int) → Int
θ(w) = Int → Int

is a solution. If we are given the constraint system

v = v → Int ,

then there is no solution because of the cycle. The forms of constraints we consider can be unsolvable for two reasons:

• there is a cycle in the constraint system such that we can obtain:

v = (… v …) → … or
v = … → (… v …) , or

• we can deduce Int = s → t.

Here is a simple algorithm for unifying two trees s and t. Initialize θ to be the identity function. Repeat the following step until θ(s) = θ(t) or failure has occurred:

• Find the leftmost symbol in s such that the corresponding symbol in t is different. Let A_{s} and A_{t} be the trees in θ(s) and θ(t) which are rooted at the symbols just found.

1. If neither A_{s} not A_{t} are variables, or one is a variable occurring in the other, then halt with failure.

2. Otherwise, suppose it A_{s} which is a variable. Update θ such that θ(A_{s}) = A_{t}.