On this page:
5.1 Syntax
5.2 Dynamic semantics
5.3 Static semantics
5.4 Church data
5.4.1 Natural numbers
5.4.2 Booleans
5.4.3 Products
5.4.4 Sums
5.4.5 Lists
5.4.6 Existentials

5 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.

5.1 Syntax🔗

5.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.

5.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.

5.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.

5.4.1 Natural numbers🔗

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

  • c0 = ,

  • c1 = ,

  • c2 = ,

  • and in general, cn as the function that for any type , iterates an function n times.

Exercise 33. Define the successor function of type .

Exercise 34. Define addition, multiplication, and exponentiation.

Exercise 35 (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.

5.4.2 Booleans🔗

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

  • tru = , and

  • fls = .

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

Exercise 36. Define not, and, and or.

Exercise 37. Define .

5.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 38. How can we write the selectors fst and snd?

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

Exercise 40. Define the recursor from our STLC extension.

5.4.4 Sums🔗

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

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

5.4.5 Lists🔗

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

Exercise 42. Define cons.

Exercise 43. Define .

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

5.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 45. 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 46. Prove type safety for λ-2.

Exercise 47 (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 48. Can we prove it?

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