This document explores the relationship between module systems, existential types and abstract data types for non-experts of type theory. I wrote this mainly to clarify my understanding of how a combination of these three leads to the development of modular programming. The theoretical part of this document is adapted from [TAPL-Pierce], and hopefully simplified enough for understanding.
In theory an existential type is written as
Well that looks like a mouthful - let's deconstruct a bit with some analogy with actual examples.
The pair
Based on the definition of abstract data types in [TAPL-Pierce], a conventional abstract data type (ADT) consists of:
- a type name
$A$ - a concrete representation type
$T$ - implementation of some operations for creating, querying and manipulating values of type
$T$ , and - an abstraction boundary enclosing the representation and operations
Here's a concrete example of an ADT in OCaml, a module for a Counter.
module type Counter = sig
type t
val make: int -> t
val inc : t -> int -> t
val get : t -> int
end
module IntCounter : Counter = struct
type t = int
let make initial_value = initial_value
let inc counter increment = counter + increment
let get counter = counter
endThe above package declares the following:
- a module signature,
Counter, or a specification of what values/types the module should contain. This signature says that aCountermodule should contain some typetas well as three functions of various types. Based on the above definition, the abstract type name of the ADT isCounter. - a module implementation,
IntCounter, that adheres to the specified signature and uses a concrete representation typeint. And the implementations of the operations that deal withCounterobjects concretely asints. - the abstraction boundary that spans across the signature and the definition of the module. Beyond this abstraction boundary, the association between
Counterandintis broken, so that the only thing that can be done with acounter.makeis to use it as an argument tocounter.incandcounter.get.
A key property of such information hiding is representation independence. We can substitute an alternative implementation of the Counter ADT, for example one where the internal representation is a record containing an int rather than just the int.
module RecordCounter: Counter = struct
type t = { x : int }
let make (n : int) : t = {x = n}
let inc (ctr : t) (n : int) : t = {x = ctr.x + n}
let get (ctr : t) : int = ctr.x
endBased on our definition above, an existential type is a pair, written Counter in the previous section. The signature Counter is the type component and the structure IntCounter or the RecordCounter form the term component of the existential.
Let's discuss a bit about how to build (or introduce) elements that inhabit existential types. It may be easier to think in terms of how we can package existential types formally.
Consider the existential package that we introduced in the first section along with the definition of existential types. The package
Now we can also say that this same package
So, how does the type checker automatically decide on the exact type of this existential package ? It can't - hence the programmer must specify which one is intended. And the simplest way to do this is just to add an annotation to every package that explicitly gives its intended type. This is how the full introduction form of existential types will look like:
The introduction form is also known as pack meaning the packaging of the package.
The dual form of introduction is elimination, which is also known as the unpack and refers to the open or import directive. It allows the components of the module to be used in some other parts of the program, but holds abstract the identity of the module's type component (look at the discussion on representation independence in the section of ADTs).
Consider our usual example of the existential package
and it will correspond to module usage in OCaml.
- [TAPL-Pierce] Types and Programming Languages - Benjamin C. Pierce