Existential types

Read TAPL 24.

Motivation

In modular and object-oriented programming, an important principle is to separate a module or object into an interface and an implementation. Users of the module or object only see the interface, and are indifferent, if there are multiple implementations, to which implementation is used.

How do we add something like this to the λ-calculus? For example, we can encode Church Booleans in System F:

CBool ≡ ∀Y. Y  Y  Y
 ctru ≡ λY. λt:Y. λf:Y. t
 cfls ≡ λY. λt:Y. λf:Y. f
ctest ≡ λb:(∀Y.YYY). λY. λt:Y. λf:Y. b [Y] t f

Or, we could use variants:

VBool ≡ <tru:Unit, fls:Unit>
 vtru ≡ <tru=unit>
 vfls ≡ <fls=unit>
vtest ≡ λb:<tru:Unit, fls:Unit>. λY. λt:Y. λf:Y.case b of
                                                  <tru=z>  t,
                                                  <fls=z>  f

But we don’t have any way to write a function that can operate on either CBools or VBools. And a function that operates on CBools could do unintended things like directly call tru [Nat] 1 0.

What we want to do is to package up CBool together with ctru, cfls, and ctest in such a way that a user only sees an opaque type together with opaque functions that operate on that type.

Existential types can do this for us. We’ve learned universal types; what’s an existential type?

In logic, the sentence ∀x. x + 0 = x can be thought of as saying, “You give me any number, and I claim that adding 0 to it gives the same number.” A proof of ∀x. x + 0 = x gives a way of mapping any particular number, say, 123, to a proof that 123 + 0 = 123.

In programming languages, the type ∀X. XX can be thought of as saying, “Give me any type X, and I can give you a term that has type XX”. One example (actually, the only example) of such a mapping is λX. λx:X. x.

In logic, the sentence ∃x. x * x = 1 could be thought of as saying, “I have a number – but am not telling you what it is – and squaring it gives 1.” One way to prove this sentence would be to exhibit a particular number, 1, and a proof that 1 * 1 = 1. Or, -1 together with a proof that -1 * -1 = 1.

So, in programming languages, the type ∃X. XX can be thought of as saying, “I have a type X – but I am not telling you what it is – and a term of type XX.” And the terms that have this existential type would exhibit one such type, say, Nat, together with a term of type NatNat, say, λx. succ x. Or, Bool, together with a term of type BoolBool, say, λx. not x. This separation between an interface (the existential type ∃X. XX) and an implementation (Nat and \x. succ x) is exactly what we want.

Syntax

Maybe I shouldn’t mess with the book’s syntax, but I did:

t ::= ...
      {*T1, t2} as ∃X1. T2
      let {*X1, x2} = t in t3

T ::= ...
      ∃X1. T2

The first form, sometimes called pack, introduces an existential. The {*T1, t2} can be thought of as the implementation, while the ∃X1. T2 is the existential type and can be thought of as the interface. The {*T1, t2} is supposed to look like a pair containing a type T1 (marked with a * to remind you that it’s not a term), which on the outside is only known as X1, and a term t2, which on the outside is only known to have type T2.

The second form, sometimes called unpack, takes an existential t and binds X1 to its first member and x2 to its second member. Then it evaluates t3.

The typing and evaluation rules can be found in the book (Figure 24-1).

Example

Because existentials are module-like, it’s common to combine them with records (TAPL 11.8), which are like tuples with named members. We omit a formal definition here, but a record type is written like {l1: T1, l2: T2} where l1 and l2 are labels, and a record value is written like {l1 = v1, l2 = v2}. If t is a record then you access its members as t.l1 and t.l2.

There’s more than one way to make use of existentials, as described in the book, but here would be one way to hide the implementation details of Booleans:

 Bools ≡ ∃B. {tru: B, fls: B, test: B  ∀Y. Y  Y  Y}

cbools : Bools
cbools ≡ {*∀Y. Y  Y  Y,
          {tru = λY. λt:Y. λf:Y. t,
           fls = λY. λt:Y. λf:Y. f,
           test = λb:(∀Y.YYY). λY. λt:Y. λf:Y. b [Y] t f}} as Bools

vbools : Bools
vbools ≡ {*<tru:Unit, fls:Unit>,
          {tru = <tru=unit>,
           fls = <fls=unit>,
           test = λb:<tru:Unit, fls:Unit>. λY. λt:Y. λf:Y.case b of
                                                          <tru=z>  t,
                                                          <fls=z>  f}} as Bools
                                                          

Then suppose we want to write the equivalent of test true 1 0 that works with either implementation:

main : Bools  Nat
main ≡ λbools:Bools. let {*B, r} = bools in
                       r.test r.tru 1 0

main cbools * 1
main vbools * 1

Next, suppose we want to add some useful functions. You might think we can write

boolutils = λbools:Bools. let {*B, r} = bools in
                            {and: λx:B. λy:B. r.test x y r.fls,
                             or: λx:B. λy:B. r.test x r.tru y,
                             not: λx:B. r.test x r.fls r.tru}

but this would not typecheck, because it tries to let B “leak” out of the let {*B, r}.

What we can do is unpack an existential and re-pack it with the functions added:

Bools' ≡ ∃B. {tru: B, fls: B, test: B  ∀Y. Y  Y  Y,
              and: B  B  B, or: B  B  B, not: B  B}
boolutils : Bools  Bools'
boolutils ≡ λbools: Bools. let {*B, r} = bools in {*B,
                             {tru: r.tru, fls: r.fls, test: r.test,
                              and: λx:B. λy:B. r.test x y r.fls,
                              or: λx:B. λy:B. r.test x r.tru y,
                              not: λx:B. r.test x r.fls r.tru}} as Bools'

Encoding existentials

In logic, we have ∃X.ϕ ¬∀X.¬ϕ (where X ranges over Booleans), which suggests that we might be able to express existentials in terms of universals. We don’t have any type operator that corresponds to negation (yet), but we do have the following more complicated equivalence not using negation:

∃X.ϕ  ∀Y. (∀X. ϕY)  Y

A second way to think about this is by analogy with pairs.

                     {T1, T2} ≡ ∀Y. (X1X2Y)Y
                     {v1, v2} ≡ λY. λf:X1X2Y. f v1 v2
(letpair {x1, x2} = t3 in t4) ≡ t3 [T4] (λx1. λx2. t3)     (where t4:T4)

Either way, we are led to the following:

                    ∃X.T ≡ ∀Y. (∀X.TY)  Y
        {*S, t} as ∃X. T ≡ λY. λf:(∀X.TY). f [S] t
(let {*X, x} = t3 in t4) ≡ t3 [T4] (λX. λx:T31. t3)   (where t3:∃X.T31 and t3:T3)