Polymorphic lambda calculus

Read TAPL 23.

Polymorphism is not a well-defined concept; TAPL 23.2 lists several varieties of polymorphism, of which the three most commonly mentioned are:

  • Subtype polymorphism is the kind seen in object-oriented programming: a reference to an object of a parent class can also refer to an object of its subclass.

  • Ad hoc polymorphism is also known as function overloading: a single name refers to two different functions with different argument types.

  • Parametric polymorphism, also known as genericity, is when a single function can be applied to values of many types. For example, the identity function λx.x ought to be applicable to values of any type.

Our focus here is on parametric polymorphism. As a running example, consider

let id = λx. x in {id 0, id true}

This term would evaluate to {0, true} in the untyped lambda calculus, but in the simply-typed lambda calculus, it will not type-check.

A more practical example (polymorphic lists) is given on pages 345-6. This is exactly analogous to the need for types like list<T> in C++ or LinkedList<T> in Java.

System F

System F enables polymorphism by allowing you to write terms that take types as arguments. For example:

let id = λX. λx:X. x in {id [Nat] 0, id [Bool] true}

The λX introduces a type abstraction, a term that takes a type as an argument. (Some authors use a capital Λ instead of a lowercase λ.) The [Nat] and [Bool] are type applications. (Some authors don’t use the square brackets.) They cause id to “morph” into a function on natural numbers and booleans, respectively.

Syntax and evaluation rules

t ::= ...
      λX. t   (type abstraction)
      t [T]   (type application)

T ::= ...
      X       (type variable)
      ∀X. T   (universal type)

v ::= ...
      λX. t


     t1  t1'
――――――――――――――――――――          (E-TApp)
t1 [T2]  t1' [T2]

(λX. t12) T2  [XT2]t12   (E-TAppTAbs)

Typing rules

The typing rules for System F now have contexts (Γ) that include both term variable bindings (x : T) and type variables (X). In rule T-TAbs,

  Γ, X ⊢ t2 : T2
———————————————————   (T-TAbs)
Γ ⊢ λX. t2 : ∀X. T2

the type variable X is added to the context when typing t2, just as the term variable x is added to the context when typing an abstraction. The book uses a convention (p. 342) whereby the same (type or term) variable can’t be added to the context twice; if this does happen, one of them should be renamed. Another way to get the same effect (p. 342 again) would be to add a side condition that X is not a free type variable in Γ.

The other new typing rule is

    Γ ⊢ t1 : ∀X. T12
――――――――――――――――――――――――   (T-TApp)
Γ ⊢ t1 [T2] : [XT2]T12

Programming in System F

System F lets us do all kinds of things that we couldn’t do in the simply-typed lambda calculus, like Church encodings of numbers, booleans, or lists (pages 347-353). In fact, we can encode product types, sum types, and even recursive types so long as the recursion is in positive position [1].

Booleans:

CBool ≡ ∀X. X  X  X
  tru ≡ λX. λt:X. λf:X. t
  fls ≡ λX. λt:X. λf:X. f

Exercise [23.4.5]: Define and. It may be helpful (and often is helpful in general) to write down the type of and first.

Numbers:

CNat ≡ ∀X. (XX)  X  X
  c0 ≡ λX. λs:XX. λz:X. z
  c1 ≡ λX. λs:XX. λz:X. s z
  c2 ≡ λX. λs:XX. λz:X. s (s z)
     ⋮

Exercise [23.4.6]: Define iszero.

Type inference?

We could also imagine an implicitly-typed System F, where the terms look just like the untyped lambda calculus, that is, no type annotations on abstractions, no type abstractions, and no type applications. This would look like the simply-typed lambda calculus with the addition of two typing rules:

 x:T ∈ Γ
—————————                            (T-Var)
Γ ⊢ x : T

  Γ, x:T1 ⊢ t2 : T2
—————————————————————                (T-Abs)
Γ ⊢ λx. t2 : T1  T2

Γ ⊢ t1 : T11T12    Γ ⊢ t2 : T11
—————————————————————————————————    (T-App)
        Γ ⊢ t1 t2 : T12

Γ, X ⊢ t : T
—————————————                        (T-Gen)
Γ ⊢ t : ∀X. T

 Γ ⊢ t1 : ∀X. T12
———————————————————                  (T-Inst)
Γ ⊢ t1 : [XT2]T12

The new rules T-Gen and T-Inst are just the rules T-TAbs and T-TApp, respectively, with type annotations, abstractions, and applications erased.

The intuition behind T-Gen is: before typing a term t, we can create a new type variable X, which may end up appearing in the type of t. (This is because T-Abs can guess a type for a term variable that uses X.) After typing t, T-Gen wraps a ∀X around its type.

The intuition behind T-Inst is: after typing t, if its type has a ∀X, we can strip it off and replace (free occurrences of) X with another type (which could be polymorphic and which could involve other type variables).

The bad news is that type reconstruction in this system is undecidable [2]. So we need to restrict the type system to make it practical for type reconstruction.

[1]
Philip Wadler. 1990. Recursive types for free! Retrieved from http://homepages.inf.ed.ac.uk/wadler/papers/free-rectypes/free-rectypes.txt
[2]
J. B. Wells. 1999. Typability and type checking in System F are equivalent and undecidable. Annals of Pure and Applied Logic 98, 1–3 (1999), 111–156. DOI:https://doi.org/10.1016/S0168-0072(98)00047-5