Existential Types and Module Systems Xiaoheng Ji Department of Computing and Software March 19, 2004
Data Abstraction The fundamental idea: the separation of the client from the implementor of the abstraction by an interface. The central ideas: –Define a representation type together with operations that manipulate values of that type. –Hold the type representation type abstract from client of the ADT to ensure representation independence. ``Abstract types have existential type'' [Mitchell and Plotkin 84] Existential types provide the fundamental linguistic mechanisms for defining interfaces, implementing them, and using the implementation in client code. Existential types are a kind of type originally developed in constructive logic.
Existential Types Two different ways of looking at an existential type, written { X, T} or X.T –logical intuition: an element of { X, T} is a value of type [X=>S]T, for some type S. –Operational intuition: an element of { X, T} is a pair, written {*S, t}, of a type S and a term t of type [X S]T. Instead of X.T, the nonstandard notation { X, T} suggests that the existential value is a mixed type-value tuple.
Existential Types - Overview Typing and evaluation rules Introducing ADTs –Introducing ADTs –Introducing objects –Objects vs. ADTs Encoding existential types
Existential introduction an existentially typed value introduced by pairing a type with a term: {*S,t} intuition: value {*S,t} of type { X,T} is a module with a type component S and a term component t, where [S/X]T. The type S is often called the hidden representation type, or sometimes the witness type of the package.
Example p = {*Nat, {a = 0, f = x : Int.succ(x)}} as { ∃ X.{a : X, f : X->X}}; p : { ∃ X.{a : X, f : X->X}} q = {*Nat, {a = 0, f = x : Int.succ(x)}} as { ∃ X.{a : X, f : X->Nat}}; q : { ∃ X.{a : X, f : X->X}} Nat – type component {a = 0, f = x : Int.succ(x)} – term component as { ∃ X.{a : X, f : X Nat}} – type annotation
Typing rule for existential introduction A value of type X.T is a package with a witness type T‘ for X and a value term t : [X -> T']T. –pack X = T' with t as T : X.T (conventional notation) –{*T', t} as { X, T} (Pierce's notation) The Introduction typing rule for existential types: |- t 1 : [X => U]T 1 |- {*U, t 1 } as { X, T 1 } : { X, T 1 } (T- PACK)
Examples of Existential types {*Nat, 0} as { X, X} : { X, X} {*Bool, true} as { X, X} : { X, X} p = {*Nat, {a = 0, f = x : Nat. succ x}} as { X, {a : X, f : X -> Nat}} p : { X, {a : X, f : X -> Nat}} q = {*Bool, {a = true, f = x : Bool. 0}} as { X, {a : X, f : X -> Nat}} q : { X, {a : X, f : X -> Nat}} The type part is hidden (opaque, abstract), and the value part provides an interface for interpreting the hidden type.
Unpacking existential values Unpacking an existential: let {X,x} = t1 in t2 Type variable X cannot occur in T2 -- it is not in scope (i.e. doesn't appear in the context ). This means that the name X of the existential witness type cannot "escape“ from the let expression. Also, within the body t2, the type X is abstract and can only be used through the interface provided by x : T1. |- t 1 : { X, T 1 } |- let {X,x} = t 1 in t 2 : T 2 (T- UNPACK) , X, x: T 1 |- t 2 : T 2
Example p = {*Nat, {a = 0, f = x : Nat. succ x}} as { X, {a : X, f : X -> Nat}} p : { X, {a : X, f : X -> Nat}} The elimination expression let { X, x} = p in ( x.f x.a) ; ⊳ 1 : Nat opens p and uses the fields of its body (x.f and x.a) to compute a numeric result.
Example p = {*Nat, {a = 0, f = x : Nat. succ x}} as { X, {a : X, f : X -> Nat}} p : { X, {a : X, f : X -> Nat}} The body of the elimination form can also involve the type variable x: let { X, x} = p in ( y:X. (x.f y) ) x.a; ⊳ 1 : Nat The fact that the package’s representation type is held abstract during the typechecking of the body means that the only operations allowed on x are those warranted by its ``abstract type'‘ {a : X, f : X->Nat}.
Example p = {*Nat, {a = 0, f = x : Nat. succ x}} as { X, {a : X, f : X -> Nat}} p : { X, {a : X, f : X -> Nat}} all operations on term x must be warranted by its abstract type, e.g. we cannot use x.a concretely as a number (since the concrete type of the module is hidden): let { X, x} = p in succ(x.a); ⊳ Error: argument of succ is not a number
Example p = {*Nat, {a = 0, f = x : Nat. succ x}} as { X, {a : X, f : X -> Nat}} p : { X, {a : X, f : X -> Nat}} In the rule T-UNPACK, the type variable X appears in the context in which t2’s type is calculated, but does not appear in the context of the rule’s conclusion. This means that the result type T2 cannot contain X free, since any free occurrences of X will be out of scope in the conclusion. let { X, x} = p in x.a; ⊳ Error: Scoping error! must add side condition to typing rule for existential elimination; X may not occur in the result type
Evaluation rule for existentials let {X, x} = ({*T 11, v 12 } as T 1 ) in t 2 [X->T 11 ] [x->v 12 ] t 2 (E- UNPACKPACK) If the first subexpression of the let has already been reduced to a concrete package, then we may substitute the components of this package for the variables X and x in the body t 2. In terms of analogy with modules, this rule can be viewed as a linking step, in which symbolic names (X and x) referring to the components of a separately compiled module are replaced by the actual contents of the module. Since the type variable X is substituted away by this rule, the resulting program actually has concrete access to the package’s internals.
Existential Types - Overview Typing and evaluation rules Introducing ADTs –Introducing ADTs –Introducing objects –Objects vs. ADTs Encoding existential types
Parametricity consider: p = {*Nat, {a = 0, f = x : Nat. 0}} as { X, {a : X, f : X -> Nat}} q = {*Bool, {a = false, f = x : Bool. 0}} as { X, {a : X, f : X -> Nat}} evaluation does not depend on the specific type of p and q: it is parametric in X: let { X, x} = p in (x.f x.a); ⊳ 0 let { X, x} = q in (x.f x.a); ⊳ 0 Idea: use parametricity to construct two kinds of programmer defined abstractions: abstract data types (ADTs) and objects
Existential Types - Overview Typing and evaluation rules Introducing ADTs –Introducing ADTs –Introducing objects –Objects vs. ADTs Encoding existential types
Abstract Data Types A conventional abstract data type (or ADT) consists of: A type name A A concrete representation type T Implementations of some operations for creating, querying, and manipulating values of type T An abstraction boundary enclosing the representation and operations Inside this boundary, elements of the type are viewed concretely (with type T). Outside, they are viewed abstractly, with type A. Values of type A may be passed around, stored in data structures, etc., but not directly examined or changed – the only operations allowed on A are those provided by the ADT.
Example signature COUNTER = sig type counter val new : counter val get : counter -> Nat val inc : counter -> counter end; abstract representation type concrete representation type interface implementation structure Counter :> COUNTER = struct type counter = Nat val new = 1 fun get(n) = n fun inc(n) = n + 1 end;
Example signature COUNTER = sig type counter val new : counter val get : counter -> Nat val inc : counter -> counter end; structure Counter :> COUNTER = struct type counter = Nat val new = 1 fun get(n) = n fun inc(n) = n + 1 end; - counter.get ( counter.inc counter.new); val it = 2 : Nat
ADTs as existentials signature COUNTER = sig type counter val new : counter val get : counter -> Nat val inc : counter -> counter end; structure Counter :> COUNTER = struct type counter = Nat val new = 1 fun get(n) = n fun inc(n) = n + 1 end; COUNTER = Counter.{ new : Counter, get : Counter -> Nat, inc : Counter -> Counter} counterADT = {*Nat, { new = 1, get(n) = n : Nat. n inc(n) = n : Nat. succ(n) }} As COUNTER ``Abstract types have existential type'‘ [Mitchell and Plotkin 84]
ADTs as existentials COUNTER = Counter.{ new : Counter, get : Counter -> Nat, inc : Counter -> Counter CounterADT = {*Nat, { new = 1, get(n) = n : Nat. n inc(n) = n : Nat. succ(n) }} As COUNTER type name Counter can be used just like a new base type e.g. we can define new ADTs with representation type Counter, e.g. a FlipFlop let {Counter, counter} = CounterADT in counter.get ( counter.inc counter.new); ⊳ 2 : Nat
Flip-Flop Let {Counter, counter} = counterADT in Let {FlipFlop, flipflop} = {*Counter, {new = counter.new, read = c : Counter. iseven (counter.get c), toggle = c : Counter. counter.inc c, reset = c : Counter. counter.new}} As { FlipFlop, {new: FlipFlop, read: FlipFlop->Bool, toggle: FlipFlop->FlipFlop, reset: FlipFlop->FlipFlop}} in flipflop.read (flipflop.toggle (flipflop.toggle flipflop.new)); ⊳ false : Bool
Representation independence Alternative implementation of the CounterADT: counterADT = {*{x:Nat}, {new = {x=1}, get = i : {x:Nat}. i.x, inc = i : {x:Nat}. {x=succ(i, x)}}} as { Counter, {new: Counter, get: Counter->Nat, inc: Counter->Counter}}; ⊳ counterADT : { Counter, {new: Counter, get: Counter->Nat, inc: Counter->Counter}} Representation independence: follows from parametricity: the whole program remains typesafe since the counter instances cannot be accessed except using ADT operations
ADT-style of programming yields huge improvements in robustness and maintainability of large systems: –limits the scope of changes to the program –encourages the programmer to limit the dependencies between parts of the program (by making the signatures of the ADTs as small as possible) –forces programmers to think about designing abstractions
Existential Types - Overview Typing and evaluation rules Introducing ADTs –Introducing ADTs –Introducing objects –Objects vs. ADTs Encoding existential types
Existential objects two basic components: internal state, methods to manipulate the state. e.g., a counter object holding the value 5 might be written c = {*Nat, {state = 5, methods = {get = x : Nat. x, inc = x : Nat. succ(x)}}} as Counter; where: Counter = { X, {state:X, methods: {get: X->Nat, inc: X->X}}};
Invoking the get method c = {*Nat, {state = 5, methods = {get = x : Nat. x, inc = x : Nat. succ(x)}}} as X, { state: X, methods: { get: X->Nat, inc: X->X}} let {X, body} = c in body.methods.get (body.state); ⊳ 5: Nat
Encapsulating the get method C = X, { state: X, methods: { get: X->Nat, inc: X->X}} sendget = c: Counter. let {X, body} = c in body.methods.get (body.state); ⊳ sendget : Counter -> Nat
Invoking the inc method c = {*Nat, {state = 5, methods = {get = x : Nat. x, inc = x : Nat. succ(x)}}} as X. { state: X, methods: { get: X->Nat, inc: X->X}} let {X, body} = c in body.methods.inc (body.state); ⊳ Error: scoping error Why? X appears free in the body of the let
Encapsulating the inc method in order to properly invoke the inc method, we must repackage the fresh internal state as a counter object: c1 = let {X, body} = c in {*X, {state = body.methods.inc (body.state), methods = body.methods}} as Counter; sendinc = c : Counter. let {X, body} = c in {*X, {state = body.methods.inc (body.state), methods = body.methods}} as Counter; ⊳ sendinc : Counter -> Counter
Example More complex operations on counters can be implemented in terms of these two basic operations: add = c : Counter. sendinc (sendinc (sendinc c)); add : Counter -> Counter
Existential Types - Overview Typing and evaluation rules Introducing ADTs –Introducing ADTs –Introducing objects –Objects vs. ADTs Encoding existential types
Abstract type of counters ADT-style: counter values are elements of the underlying representation (i.e. simple numbers of type Nat ) object-style: each counter is a whole module, including not only the internal representation but also the methods. Type Counter stands for the whole existential type: X. { state: X, methods: { get: X->Nat, inc: X->X}}
Stylistic advantages advantage of the object-style: since each object chooses its own representation and operations, different implementations of the same object can be freely intermixed advantage of the ADT-style: binary operations (i.e. operations that accept >= 2 arguments of the abstract type) can be implemented, contrary to objects
Binary operations and the object-style e.g. set objects type: NatSet = { X, {state: X, methods: {empty: X, singleton: Nat->X, member: X->Nat->Bool, union: X->NatSet->X}}} cannot implement the method since it can have no access to the concrete representation of the second argument in reality, mainstream OO languages such as C++ and Java have a hybrid object model that allows binary operations (with the cost of restricting type equivalence)
Existential Types - Overview Typing and evaluation rules Introducing ADTs –Introducing ADTs –Introducing objects –Objects vs. ADTs Encoding existential types
Duality universal types: ∀ X.T is a value of type [S/X]T for all types S. existential types: ∃ X.T is a value of type [S/X]T for some type S. idea: exploit duality to encode existential types using universal types, using the equality: ∃ X.T = ¬ ∀ X.¬T def
Encoding encoding existential types using universal types: { ∃ X,T} = ∀ Y. ( ∀ X. T->Y) -> Y. operational view: a module is a value that gets a result type and a continuation, then calls the continuation to yield the final result A continuation is something that we can call and forget about because it does not return control to the caller. def
Encoding existential elimination given: let {X, x} = t 1 in t 2 where t 1 : ∀ Y. ( ∀ X. T -> Y) -> Y first apply to result type T 2 to get type ( ∀ X. T -> >T 2 ) -> T 2 : let {X, x} = t 1 in t 2 = t 1 T 2 … then apply to continuation of type ∀ X. T -> T 2 to get result type T 2 : let {X, x} = t 1 in t 2 = t 1 T 2 ( X. x : T.t 2 ) def
Encoding existential introduction given: {*S, t} as { ∃ X, T } we must use S and t to build a value of type ∀ Y. ( ∀ X. T -> Y) -> Y begin with two abstractions: {*S, t} as { ∃ X, T } = Y. f ( ∀ X. T -> Y) … apply f to appropriate arguments: first, supply S: {*S, t} as { ∃ X, T } = Y. f ( ∀ X. T -> Y). f S … then supply t of type S to get result type Y: {*S, t} as { ∃ X, T } = Y. f ( ∀ X. T -> Y). f S t def
Existential types - summary existential types are another form of polymorphism parametricity of existentials leads to representation independence trade-offs between ADTs and objects: –ADTs support binary operations, objects do not –objects support free intermixing of different implementations, ADTs do not existentials can be encoded using universal types
Module systems - overview The OCaml Module System Haskell Modules
The OCaml Module System three key parts: structures: are packaged environments. They consist of a group of core ML objects (values, types, exceptions) and can be manipulated as a single unit. module Name = struct implementation end signatures: are the ``types’’ of structures. Corresponding to each structure one can derive a signature which consists of the names of the objects in the structure and type information for the objects. module type Name = sig signature end functors: are mappings taking structures to structures. These are useful in specifying ``generic’’ packages that given any structure with a given signature can generate a new structure with some other signature. module Name = functor (ArgName : ArgSig) -> struct implementation end module Name (Arg : ArgSig) = struct implementation end
OCaml – Modules: Values Collection of named (mutually-recursive) values: module IntListSet = struct let empty = [] let add = fun i is -> i :: List.filter (fun j -> i != j) is let asList = fun is -> is end Access using dot-notation: val ok : int list let ok = IntListSet.add 1 IntListSet.empty Modules (structures) have types (signatures): module type INTLISTSET = sig val empty : int list val add : int -> int list -> int list val asList : int list -> int list end
OCaml – Modules: Types May also include named (data) types: module IntListSet = struct type t = int list let empty = [] let add = fun i is -> i :: List.filter (fun j -> i != j) is let asList = fun is -> is end val ok : IntListSet.t let ok = IntListSet.add 1 IntListSet.empty And named (nested) modules.
OCaml – Modules: Abstract Types May also include abstract types: module type INTSET = sig type t --- abstract val empty : t val add : int -> t -> t val asList : t -> int list end module IntSet : INTSET = struct type t = int list --- concrete let empty = [] let add = fun i is -> i :: List.filter (fun j -> i != j) is let asList = fun is -> is end val ok : IntListSet.t let ok = IntListSet.add 1 IntListSet.empty val fail : int list let fail = IntListSet.add 1 [2] -- type error: incompatible types int list and IntListSet.t
OCaml – Modules: Separate Compilation Top-level modules may correspond with compilation units. True separate compilation is possible if each top-level module is accompanied by a top-level signature. intSet.mli: type t val empty : t val add : int -> t -> t val asList : t -> int list intSet.ml: type t = int list let empty = [] let add = fun i is -> i :: List.filter (fun j -> i != j) is let asList = fun is -> is Compiler never needs to look at intSet.ml when compiling other modules – only intSet.mli.
OCaml – Modules: Value Parameterisation May be parameterised by the values within other modules(functors): module type INTORD = sig val less : int -> int -> bool end module MkIntBinTreeSet = functor (IntOrd : INTORD) -> struct type t = Leaf | Node of bintree * int * bintree let empty = Leaf let rec add = fun i t -> match t with | Leaf -> Node (Leaf, i, Leaf) | Node (l, j, r) -> if IntOrd.less i j then Node (add i l, j, r) else... let rec asList = … end module IntOrd = struct let less = (<) end module IntBinTreeSet = MkIntBinTreeSet IntOrd
OCaml – Modules: Type Parameterisation May be parameterised by the types within other modules: module type ORD = sig type t val less : t -> t -> bool end module MkBinTreeSet = functor (Ord : ORD) -> struct type t = Leaf | Node of bintree * Ord.t * bintree let empty = Leaf let rec add = fun x t -> … let rec asList = … end Module IntOrd = struct type t = int let less = (<) end Module IntBinTreeSet = MkBinTreeSet IntOrd
OCaml – Modules: Type Sharing Interaction of –Separate compilation, and –Parameterisation by types Requires special support m.mli: module type SET = sig type elt type t val empty : t val add : elt -> t -> t val asList : t -> elt list end module type MKBINTREESET = functor (Ord : ORD) -> SET module MkBinTreeSet : MKBINTREESET
OCaml - Modules: Type Sharing Interaction of –Separate compilation, and –Parameterisation by types Requires special support x.ml: module IntOrd = struct type t = int let less = (<) end module IntBinTreeSet = M.MkBinTreeSet IntOrd val fail : IntBinTreeSet.t let fail = IntBinTreeSet.add 1 IntBinTreeSet.empty -- type error: incompatible types int and IntBinTreeSet.elt
OCaml – Modules: Type Sharing (2) We need to capture the sharing of types between the argument and result of the functor MkBinTreeSet. m.mli: module type SET = sig type elt type t val empty : t val add : elt -> t -> t val asList : t -> elt list end module type MKBINTREESET = functor (Ord : ORD) -> (SET with type elt = Ord.t) module MkBinTreeSet : MKBINTREESET
OCaml – Modules: Type Sharing (2) We need to capture the sharing of types between the argument and result of the functor MkBinTreeSet. x.ml: module IntOrd = struct type t = int let less = (<) end module IntBinTreeSet = M.MkBinTreeSet IntOrd val fail : IntBinTreeSet.t let fail = IntBinTreeSet.add 1 IntBinTreeSet.empty -- ok, since int = IntOrd.t = IntBinTreeSet.elt
The Haskell module system ``Haskell has a very simple module system -- a flat module namespace with the ability to import and export various entities, hide names, and specify that module qualification (M.x) is required. It also provides support for abstract data types by allowing one to export a data type without its constructors. ’’ -- Paul Hudak, 1998
The Haskell Module System Module headers module Ant where data Ants = … anteater x = … The convention for file names is that a module Ant resides in the Haskell file Ant.hs or Ant.lhs.
The Haskell Module System Importing a module module Bee where import Ant beeKeeper = … This means that the visible definitions from Ant can be used in Bee. By default the visible definitions in a module are those which appear in the module itself module Cow where import Bee
The Haskell Module System The main module Each system of modules should contain a top-level module called Main, which gives a definition to the name main. Note that a module with no explicit name is treated as Main.
The Haskell Module System Export controls –we might wish not to export some auxiliary functions. –We perhaps want to export some of the definitions we imported from other modules. We can control what is exported by following the name of the module with a list of what is to be exported: module Bee ( beeKeeper, Ants(..), anteater ) where … We follow the type name with (..) to indicate that the constructors of the type are exported with the type itself; if this is omitted, then the type acts like an abstract data type.
The Haskell Module System We can also state that all the definitions in a module are to be exported: module Bee ( beeKeeper, module Ant ) where … or equivalently module Bee ( module Bee, module Ant ) where … The simpler header module Fish where is equivalent to module Fish ( module Fish ) where
The Haskell Module System Import controls we can control how objects are to be imported: import Ant ( Ants(..) ) stating that we want just the type Ants we can alternatively say which names we wish to hide: import Ant hiding ( anteater )
The Haskell Module System Suppose that in our module we have a definition of bear, and also there is an object named bear in the module Ant. –Use the qualified name Ant.bear for the imported object, reserving bear for the locally defined one. To use qualified names we should make the import thus: import qualified Ant Give a local name Insect to a imported module Ant: import Insect as Ant
The Haskell Module System The standard prelude –Prelude.hs is implicitly imported into every module. –Modify this import, perhaps hiding one or more bindings thus: module Eagle where import Prelude hiding (words) –A re-definition of a prelude function cannot be done ``invisibly’’. We have explicitly to hide the import of the name that we want to re-define. –If we also wish to have access to the original definition of words we can make a qualified import of the prelude, import qualified Prelude and use the original words by writing its qualified name Prelude.words.
Example - Stacks A stack implemented as an ADT module can be defined as follows: module Stack(Stack,push,pop,top,emptyStack,stackEmpty) where push :: a -> stack a -> Stack a pop :: Stack a -> Stack a top :: Stack a -> a emptyStack :: Stack a stackEmpty :: Stack a -> Bool
Example - Stacks A first implementation can be done using a user-defined type: data Stack a = EmptyStk | Stk a (Stack a) push x s = Stk x s pop EmptyStk = error ``pop from an empty stack’’ pop (Stk _ s) = s top EmptyStk = error ``top from an empty stack’’ top (Stk x _) = x emptyStack = EmptyStk stackEmpty EmptyStk = True stackEmpty _ = False
Example - Stacks Another possible implementation can be obtained using the predefined list data structure because push and pop are similar to the (:) and tail operations. newtype Stack a = Stk [a] push x (Stk xs) = Stk (x:xs) pop (Stk []) = error ``pop from an empty stack’’ pop (Stk (_:xs)) = Stk xs top (Stk []) = error ``top from an empty stack’’ top (Stk (x:_)) = x emptyStack = Stk [] stackEmpty (Stk []) = True stackEmpty (Stk _ ) = False
Thank You Very Much! References: Types and Programming Languages Abstract Types Have Existential Type First-Class Modules for Haskell