On this page:
4.1 Syntax
4.2 Dynamic semantics
4.3 Static semantics
4.4 Church data
4.4.1 Natural numbers
4.4.2 Booleans
4.4.3 Products
4.4.4 Sums
4.4.5 Lists
4.4.6 Existentials

4 The polymorphic lambda calculus λ-2

Suppose we want to write the composition function in the simply-typed lambda calculus. What does it look like? Well, it depends on the types of the functions. We can compose two functions with this:

But if the functions have different types, we will need to define a different composition function. This is awkward!

Polymorphism lets us write one composition function that works for any types. We introduce type variables and abstract over them with :

We model polymorphism with λ-2, also known as System F.

4.1 Syntax

4.2 Dynamic semantics

To give the dynamic semantics of λ-2, we first define values and the evaluation contexts:

Then the reduction relation has two rules, one for value abstraction applications, and one for type abstraction applications:

The dynamic semantics of λ-2 is given by the evaluation function eval:

eval() =

 

if

As defined, eval could be partial, as with STLC, it is total on well typed terms.

4.3 Static semantics

To give the static semantics of λ-2, we have both type variable environments (which tell us which type variables are in scope) and typing environments (which map variables to their types):

The main typing judgment relies on two auxiliary judgments. The first tells us whether a type is well formed (which for this language just means closed):

A typing environment is well formed when all the types in it are well formed:

Finally, we give the typing judgments for λ-2:

Strictly speaking, every Δ and every type well-formedness premiss can go away and it all still works, with type variables acting like constants, but I like knowing where my free type variables are.

4.4 Church data

λ-2, made of lambdas big and small, may seem to lack much in the way of data, but in fact it is very rich. Alonzo Church showed how to represent natural numbers and datatypes in the untyped lambda calculus. STLC is too weak for those encodings to be meaningful, but they work beautifully in λ-2.

4.4.1 Natural numbers

The natural numbers can be defined as functions that iterate a function. In particular, define type to be , with

Exercise 26. Define the successor function of type .

Exercise 27. Define addition, multiplication, and exponentiation.

Exercise 28 (hard). Define the predecessor function.

Once we have predecessor we can define subtraction, equality, less-than, and more, but we need a bit more technology before we can define predecessor.

4.4.2 Booleans

The Booleans can be defined as their own elimination rule. In particular, let type = . Then define:

There’s no need for if-then-else—just apply the Boolean.

Exercise 29. Define not, and, and or.

Exercise 30. Define .

4.4.3 Products

In general, we can represent datatypes by their elimination principles. For example, we represent the product as a function of type . That is, a pair of a and a is a function that, for any type , you can give it a function that turns a and into an , and it gives back that .

The pair value of type is represented as .

Exercise 31. How can we write the selectors fst and snd?

Exercise 32. Now predecessor becomes possible. The idea is to apply the Nat to count upward, but using a pair to institute a delay.

Exercise 33. Define the recursor from our STLC extension.

4.4.4 Sums

We can represent the sum type as its elimination rule . (Write that out in infix.)

Exercise 34. How can we construct sum values? How do we use them?

4.4.5 Lists

We can represent a list using its elimination rule, in particular, the type of its fold. Let = .

Exercise 35. Define cons.

Exercise 36. Define .

Exercise 37. Define empty?, first, and rest.

4.4.6 Existentials

We can encode existential types in λ-2. An existential type lets us hide part of the representation of a type, and then safely use it without revealing the representation.

Define to be .

To create an existential, we must have a value that has some actual type , but we wish to view as type . There must be some type such that = . Then to create the existential, we write:

To use the existential, apply it!

For example, suppose we want to create a value of type that lets us work abstractly with the naturals. (In particular, we represent a triple containing zero of abstract type, the successor function of abstract type, and a projection that reveals the underlying natural.) We could pack that up as:

Counter =

Then to count to 2, we might write:

This is the basis of abstract types as the appear in module and object systems. Of course, it gets a bit easier to read if we add record types and make existentials primitive or, better yet, hidden.

Exercise 38. Add existentials to λ-2 without encoding as universals. In particular, you will need forms for packing and unpacking whose statics and dynamics agree with the encoding above.

Exercise 39. Prove type safety for λ-2.

Exercise 40 (Very difficult). Prove normalization for λ-2.

Consider this alternate definition of Counter:

Counter =

It should be indistinguishable from the original definition in all contexts.

Exercise 41. Can we prove it?

Exercise 42. Write a generic sorting function that takes a vector and a comparison function and sorts the vector in place.