Data Abstraction, Existential Types and Module Systems

Download Report

Transcript Data Abstraction, Existential Types and Module Systems

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
– {*T', t} as {X, T}

(conventional notation)
(Pierce's notation)
The Introduction typing rule for existential types:
 |- t1 : [X => U]T1
 |- {*U, t1} as {X, T1} : {X, T1}
(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
 |- t1 : {X, T1}
, X, x: T1 |- t2 : T2
 |- let {X,x} = t1 in t2 : T2


(T- UNPACK)
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.
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} = ({*T11, v12} as T1) in t2
 [X->T11] [x->v12] t2



(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 t2.
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;
COUNTER =
 Counter.{
new : Counter,
get : Counter -> Nat,
inc : Counter -> Counter}

structure Counter :> COUNTER =
struct
type counter = Nat
val new = 1
fun get(n) = n
fun inc(n) = n + 1
end;
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
let {Counter, counter} = CounterADT in
counter.get ( counter.inc counter.new);
⊳ 2 : Nat


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
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:
def
∃X.T = ¬∀X.¬T
Encoding



encoding existential types using universal types:
def
{∃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.
Encoding existential elimination

given:
let {X, x} = t1 in t2
where t1: ∀Y. (∀X. T->Y) -> Y
 first apply to result type T2 to get type (∀X. T-> >T2) -> T2:
def
let {X, x} = t1 in t2 = t1 T2…
 then apply to continuation of type ∀X. T->T2 to get result
type T2:
def
let {X, x} = t1 in t2 = t1 T2 (X. x : T.t2)
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:
def
def
{*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
pop
top
emptyStack
stackEmpty
::
::
::
::
::
a -> stack
Stack a ->
Stack a ->
Stack a
Stack a ->
a -> Stack a
Stack a
a
Bool
Example - Stacks

A first implementation can be done using a user-defined type:
data Stack a
=
|
push x s
=
pop EmptyStk
=
pop (Stk _ s)
=
top EmptyStk
=
top (Stk x _)
=
emptyStack
=
stackEmpty EmptyStk =
stackEmpty _
=
EmptyStk
Stk a (Stack a)
Stk x s
error ``pop from an empty stack’’
s
error ``top from an empty stack’’
x
EmptyStk
True
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
push x (Stk xs)
pop (Stk [])
pop (Stk (_:xs))
top (Stk [])
top (Stk (x:_))
emptyStack
stackEmpty (Stk [])
stackEmpty (Stk _ )
=
=
=
=
=
=
=
=
=
Stk [a]
Stk (x:xs)
error ``pop from an empty stack’’
Stk xs
error ``top from an empty stack’’
x
Stk []
True
False
References:

Types and Programming Languages

Abstract Types Have Existential Type

First-Class Modules for Haskell
Thank You Very Much!