Escolar Documentos
Profissional Documentos
Cultura Documentos
8: Abstract Datatypes
Plan
Plan
Properties of counters
We might also require that our representation should satisfy some obvious properties. The following should hold for any counters c1 and c2: is_zero(make_counter()) not is_zero(inc c) c1 = c2 iff inc(c1) = inc(c2) c1 = c2 implies that dec(c1) = dec(c2) c1 = dec(inc(counter)) Questions: do the requirements above allow us to show that inc(c1) = c1 holds for no counter c1? does the requirements above state everything we would like to be true for counters?
FP
8.1
FP
8.2
Plan
Plan
Implementing counters
type counter = int fun fun fun fun make_counter () = 0 inc c = c+1 dec c = if c = 0 then 0 else c-1 is_zero c = c = 0
Counters as lists
We could do something more imaginative: type counter = unit list fun fun fun fun make_counter () = [] inc c = ()::[] dec c = if c = [] then [] else tl c is_zero c = null c
FP
8.3
FP
8.4
Plan
Plan
Now, note that none of the representations oer any mechanism for protecting the data from misuse.
FP
8.5
FP
8.6
Plan
Plan
Why abstract datatypes? Useful to make a datatype abstract when the implementation of the datatype is complex, there are many ways to implement the datatype, or you want to keep the datatype separate from the rest of the code. The datatype represents a natural abstraction. Abstract datatypes are often a good way to split a program into parts that can be understood individually. What is an abstraction (in Computer Science)?
What is an abstraction (in Computer Science)? A way to introduce new concepts that are meaningful to humans. Examples: les, data structures, procedure calls, other programming constructs. (We think of les as something real, but les dont exist, they are just a bunch of bits on a hard drive. Come to think of it, bits dont exist either, they are just magnetic uctuations on the surface of the disk platters.)
FP
8.7
FP
8.8
Formal semantics
value emptyStack TYPE: stack VALUE: the empty stack function isEmptyStack S TYPE: stack bool PRE: (none) POST: true if S is empty false otherwise function push v S TYPE: stack stack PRE: (none) POST: the stack S with v added as new top element function top S TYPE: stack PRE: S is non-empty POST: the top element of S function pop S TYPE: stack stack PRE: S is non-empty POST: the stack S without its top element
isEmptyStack emptyStack = true v,S : isEmptyStack (push v S) = false top emptyStack = ... error ... v,S : top (push v S) = v pop emptyStack = ... error ... v,S : pop (push v S) = S
Question: Do the properties above state everything we need to know about stacks? Is there any other property you would like to add?
FP
8.9
FP
8.10
Version 2
Denition of a new constructed type using the list type:
datatype stack = Stack of list REPRESENTATION CONVENTION: the head of the list is the top of the stack, the 2nd element of the list is the element below the top, etc
type stack = list REPRESENTATION CONVENTION: the head of the list is the top of the stack, the 2nd element of the list is the element below the top, etc
Realisation of the operations (stack1.sml)
val emptyStack = Stack [ ] fun isEmptyStack (Stack S) = (S = [ ]) fun push v (Stack S) = Stack (v::S) fun top (Stack [ ]) = error "top: empty stack" | top (Stack (x::xs)) = x fun pop (Stack [ ]) = error "pop: empty stack" | pop (Stack (x::xs)) = Stack xs
val emptyStack = [ ] fun isEmptyStack S = (S = [ ]) fun push v S = v::S fun top [ ] = error "top: empty stack" | top (x::xs) = x fun pop [ ] = error "pop: empty stack" | pop (x::xs) = xs
The operations are now only dened for stacks This realisation does not force the usage of the stack type The operations can also be used with objects of type list, even if they do not represent stacks! It is possible to access the elements of the stack without using the operations specied above: no encapsulation! It is still possible to access the elements of the stack without using the operations specied above, namely by pattern matching
FP
8.11
FP
8.12
An abstract datatype (stack2.sml) Objective: encapsulate the denition of the stack type and its operations in a parameterised abstract datatype
abstype a stack = Stack of a list with val emptyStack = Stack [ ] fun isEmptyStack (Stack S) = (S = [ ]) fun push v (Stack S) = Stack (v::S) fun top (Stack [ ]) = error "top: empty stack" | top (Stack (x::xs)) = x fun pop (Stack [ ]) = error "pop: empty stack" | pop (Stack (x::xs)) = Stack xs end
abstype a stack = Stack of a list with . . . ; type a stack val a emptyStack = - : a stack val a isEmptyStack = fn : a stack -> bool ... push 1 (Stack [ ]) ; Error: unbound variable or constructor: Stack push 1 emptyStack ; val it = - : int stack
emptyStack = emptyStack ; Error: operator and operand dont agree [equality type required]
The stack type is an abstract datatype (ADT) The concrete representation of a stack is hidden An object of the stack type can only be manipulated via the functions dened in its ADT declaration The Stack constructor is invisible outside the ADT It is now impossible to access the representation of a stack outside the declarations of the functions of the ADT The parameterisation allows the usage of stacks of integers, reals, strings, integer functions, etc, from a single denition!
It is impossible to see the contents of a stack without popping its elements, so let us add a visualisation function:
function showStack S TYPE: stack list PRE: (none) POST: the representation of S in list form, with the top of S as head, etc abstype a stack = Stack of a list with ... fun showStack (Stack S) = S end
The result of showStack is not of the stack type One can thus not apply the stack operations to it
FP
8.13
FP
8.14
Version 3
Denition of a recursive new constructed type:
datatype stack = EmptyStack | >> of stack inx >> EXAMPLE: EmptyStack >> 3 >> 5 >> 2 represents the stack with top 2 REPRESENTATION CONVENTION: the right-most value is the top of the stack, its left neighbour is the element below the top, etc
An abstract datatype (stack3.sml)
abstype a stack = EmptyStack | >> of a stack a with inx >> val emptyStack = EmptyStack fun isEmptyStack EmptyStack = true | isEmptyStack (S>>v) = false fun push v S = S>>v fun top EmptyStack = error "top: empty stack" | top (S>>v) = v fun pop EmptyStack = error "pop: empty stack" | pop (S>>v) = S fun showStack EmptyStack = [ ] | showStack (S>>v) = v :: (showStack S) end
We have thus dened a new list constructor, but with access to the elements from the right !
FP
8.15
FP
8.16
function dequeue Q TYPE: queue queue PRE: Q is non-empty POST: the queue Q without its head element function showQueue Q TYPE: queue list PRE: (none) POST: the representation of Q in list form, with the head of Q as head, etc
Operations
value emptyQueue TYPE: queue VALUE: the empty queue function isEmptyQueue Q TYPE: queue bool PRE: (none) POST: true if Q is empty false otherwise function enqueue v Q TYPE: queue queue PRE: (none) POST: the queue Q with v added as new tail element function head Q TYPE: queue PRE: Q is non-empty POST: the head element of Q
Formal semantics
isEmptyQueue emptyQueue = true v,Q : isEmptyQueue (enqueue v Q) = false head emptyQueue = ... error ... v,Q : head (enqueue v Q) = if isEmptyQueue Q then v else head Q dequeue emptyQueue = ... error ... v,Q : dequeue (enqueue v Q) = if isEmptyQueue Q then emptyQueue else enqueue v (dequeue Q)
Question: Do the properties above state everything we need to know about stacks? Is there any other property you would like to add?
FP
8.17
FP
8.18
Version 2
Representation of a FIFO queue by a pair of lists :
type queue = list REPRESENTATION CONVENTION: the head of the list is the head of the queue, the 2nd element of the list is behind the head of the queue, and so on, and the last element of the list is the tail of the queue
Example: the queue
head 3 8 7 5 0 tail 2
datatype queue = Queue of list list REPRESENTATION CONVENTION: the term Queue ([x1, x2, . . . , xn], [y1, y2, . . . , ym ]) represents the queue
head tail
x1 x2
. . . xn ym
...
y2 y1
It is now possible to enqueue in (1) time It is still possible to dequeue in (1) time, but only if n 1 What if n = 0 while m > 0?! The same queue can thus be represented in dierent ways How to test the equality of two queues?
is represented by the list [3,8,7,5,0,2] Exercises Realise the queue ADT using this representation What is the time complexity of enqueuing an element? What is the time complexity of dequeuing an element?
FP
8.19
FP
8.20
Normalisation Objective: avoid the case where n = 0 while m > 0 When this case appears, transform (or: normalise) the representation of the queue: transform Queue ([ ], [y1, . . . , ym ]) with m > 0 into Queue ([ym , . . . , y1 ], [ ]), which indeed represents the same queue We thus have:
function normalise Q TYPE: queue queue PRE: (none) POST: if Q is of the form Queue ([ ], [y1, . . . , ym ]) then Queue ([ym, . . . , y1 ], [ ]) else Q
Realisation of the operations (queue2.sml) Construction of an abstract datatype: the normalise function may be local to the ADT, as it is only used for realising some operations on queues
abstype a queue = Queue of a list a list with val emptyQueue = Queue ([ ],[ ]) fun isEmptyQueue (Queue ([ ],[ ])) = true | isEmptyQueue (Queue (xs,ys)) = false fun head (Queue (x::xs,ys)) = x | head (Queue ([ ],[ ])) = error "head: empty queue" | head (Queue ([ ],y::ys)) = error "head: non-normalised queue" local fun normalise (Queue ([ ],ys)) = Queue (rev ys,[ ]) | normalise Q = Q in fun enqueue v (Queue (xs,ys)) = normalise (Queue (xs,v::ys)) fun dequeue (Queue (x::xs,ys)) = normalise (Queue (xs,ys)) | dequeue (Queue ([ ],[ ])) = error "dequeue: empty queue" | dequeue (Queue ([ ],y::ys)) = error "dequeue: non-norm. queue" end fun showQueue (Queue (xs,ys)) = xs @ (rev ys) fun equalQueues Q1 Q2 = (showQueue Q1 = showQueue Q2) end
Why do the head and dequeue functions not normalise the queue instead of stopping the execution with an error? The normalisation and representation invariant are hidden in the realisation of the abstract datatype On average, the time of enqueuing and dequeuing is (1) This representation is thus very ecient!
FP
8.21
FP
8.22
How do I justify the constant time complexity? A simple amortized analysis Suppose enQueue an element costs 1 krona. deQueue an elements costs 1 krona if we do not need to reverse 1 + n krona if we reverse a list of length n Make the cost of adding an element to the queue two kronor, and the cost of removal 1 krona Each time we add an element we use 1 krona and keep 1. When deQueue need to reverse a listthere is money in the savings account to pay for it. Conclusion: a single deQueue operation may be costly but a sequence of n enQueue and deQueue (starting with an empty queue) has cost proportional to n. The average cost of deQueue has a constant bound.
Tables Some operations: empty_table create an empty table insert k v t insert a key k with associated value v into the table t lookup k t look for the key k in the table t, if there is a key k with corresponding value v, return SOME v else return NONE to_list t return a list of key-value pairs
FP
8.23
FP
8.24
A simple implementation of tables: association lists val empty_table = [] fun insert k v t = (k,v)::t fun lookup k [] = NONE | lookup k ((k,v)::t) = if k=k then SOME v else lookup k t fun to_list t = t fun from_list [] = empty_table | from_list ((k,v)::xs) = insert k v (from_list xs)
Notes on association lists every computer scientist should know what an alist is! inecient (why?) what is their advantage? the list may contain duplicate keys. Does not aect search, but result of to_list may be unsatisfactory polymorphic in keys and values, but there is a requirement on keys A better to_list fun to_list t = let fun tl [] acc = rev acc | tl ((k,v)::xs) acc = (case lookup k acc of NONE => tl xs (insert k v acc) | SOME _ => tl xs acc) in tl t [] end
FP
8.25
FP
8.26
Association lists, test run Test run: val val val val val val val empty_table = [] : a list insert = fn : a -> b -> (a * lookup = fn : a -> (a * b) to_list = fn : a -> a to_list = fn : (a * b) list from_list = fn : (a * b) list it = () : unit b) list -> (a * b) list list -> b option -> (a * b) list -> (a * b) list
Functional tables Idea: represent a function as a table (see also previous lecture). First, two types for the operation and result: datatype (a, b) table_op = Insert of (a * b) | Lookup of a | ToList datatype (a, b) result = Table of (a,b) table_op -> (a, b) result | Found of b option | List of (a*b) list Empty tables fun empty_table (Insert(k,v)) = Table (non_empty_table k v empty_table) | empty_table (Lookup k) = Found NONE | empty_table ToList = List [] Non-empty tables fun non_empty_table k v t (Insert(k,v)) = Table (non_empty_table k v (non_empty_table k v t)) | non_empty_table k v t (Lookup k) = if k=k then Found (SOME v) else t (Lookup k) | non_empty_table k v t ToList = let val List l = (t ToList) in List ((k,v)::l) end
- from_list [("one", 1),("two", 2), ("three", 3), ("four", 4)]; val it = [("one",1),("two",2),("three",3), ("four",4)] : (string * int) list - from_list [("one", 1),("two", 2), ("three", 3), ("four", 4)]; val it = [("one",1),("two",2),("three",3), ("four",4)] : (string * int) list - insert "four" 5 it; val t = [("four",5),("one",1),("two",2),("three",3), ("four",4)] : (string * int) list - to_list t; val it = [("four",5),("one",1),("two",2),("three",3), ("four",4)] : (string * int) list - to_list t; val it = [("four",5),("one",1),("two",2), ("three",3)] : (string * int) list -
FP
8.27
FP
8.28
Tables as binary search trees datatype b bsTree = Void | Bst of int * b * b bsTree * b bsTree val empty_table = Void fun insert k v Void = Bst(k,v,Void,Void) | insert k v (Bst(key,value,L,R)) = if k = key then Bst(k,v,L,R) else if k < key then Bst(key,value, (insert k v L), R) else Bst(key, value, L, (insert k v R)) fun lookup k Void = NONE | lookup k (Bst(key,value,L,R)) = if k = key then SOME value else if k < key then lookup k L else lookup k R
t (Insert(k,v))
fun lookup k t = let val Found f = t (Lookup k) in f end fun to_list t = let val List l = t ToList in l end
FP
8.29
FP
8.30
Functional tables, conversion to lists A simple conversion fun to_list Void = [] | to_list (Bst(key,value,L,R)) = (to_list L) @ [(key, value)] @ (to_list R)
But our binary search trees are not polymorphic Use of integer comparison means that only integers can be used as keys. Solutions: allow extra parameter: insert order key val t store order in data structure (how?) dene higer-order function that creates specialized functions
A more ecient one fun to_list t = let fun t_l Void acc = acc | t_l (Bst(key,value,L,R)) acc = t_l L ((key, value)::(t_l R acc)) in t_l t [] end
val (empty_table, insert, lookup, to_list) = make_bst_adt (String.<) (this is easier than it looks!)
FP
8.31
FP
8.32
Sequences empty_seq create empty sequence % create singleton sequence $ concatenate two sequences (so that given sequences s1 and s2 we obtain a new sequence s1 $ s2) seqmap is like map on lists seq2list converts a sequence to a list
Sequences, the obvious solution val empty_seq = [] fun % x = [x] nonfix $ val $ = op @ infix 2 $ val seqmap = List.map val seq2list = fn x => x Lets look at the types val empty_seq = [] : a list val % = fn : a -> a list val $ = fn : a list * a list -> a list val seqmap = fn : (a -> b) -> a list -> b list val seq2list = fn : a -> a Not very interesting. Also, not very ecient if we do a lot of concatenations.
FP
8.33
FP
8.34
Sequences as trees infix 2 $ datatype a seq = $ of a seq * a seq | % of a | empty_seq fun seqmap f (s1 $ s2) = (seqmap f s1 $ seqmap f s2) | seqmap f (% l) = (f l) | seqmap f empty_seq = empty_seq fun seq2list s = let fun f (s1 $ s2, l) = f(s1, f(s2, l)) | f (% a, l) = a :: l | f (empty_seq, l) = l in f (s, []) end
Polynomials Examples x+1 x*x+100*x 42 x^100 Some operations constant c create a polynomial representing a constant timesX p multiply a polynomial with x eval p v determine the value of the polynomial, given that x=v add p1 p2 add two polynomials
FP
8.35
FP
8.36
Polynomials as lists Example: the polynomial 2x4 + 5x3 + x2 + 3 can be represented by the list [3,0,1,5,2] In general: the list [a0, a1, . . . , an] with an = 0 represents the polynomial P n (x) = an xn + + a1 x + a0 We assume integer coecients and natural-number powers type poly = int list fun constant c = [c]
Sparse polynomials
What if a lot of coecients are zero?! Example: 3x27 + 4x5 + 3x2 In the preceding representation: High memory consumption High run time of the operations (many evaluation steps) We need a better representation! Representation of sparse polynomials
fun timesX p = 0::p fun eval [] v = 0 | eval (a::p) v = (eval p v) * v + a fun add p1 [] = p1 | add [] p2 = p2 | add (a::p1) (b::p2) = (a+b) :: add p1 p2
Example: the polynomial 3x27 + 4x5 + 3x2 can be represented by the list [(2,3), (5,4), (27,3)] In general: the list [(k1, c1), . . . , (km, cm )] with: ci = 0 for 1 i m ki 0 for 1 i m ki < ki+1 for 1 i < m represents the polynomial cm xkm + + c1xk1
FP
8.37
FP
8.38
type poly = (int*int) list fun constant c = [(0,c)] fun timesX [] = [] | timesX ((k,c)::p) = (k+1,c)::timesX p fun expo x 0 = 1 | expo x n = x * expo x (n-1) fun eval [] v = 0 | eval ((k, c)::p) v =
Sparse polynomials, questions When, if ever, is the sparse polynomial representation more ecient than the rst? Is it meaningful to compare polynomials for equality, i.e., will one polynomial always have the same representation? Does the function add remind you of any other function you have seen? There is a little bug in the sparse polynomials. Can you nd it? Can you see any way to improve performance?
c*(expo v k)+(eval p v)
fun add [] p = p | add p [] = p | add ((k1,c1)::p1) ((k2, c2)::p2) = if k1 < k2 then (k1,c1)::(add p1 ((k2, c2)::p2)) else if k2 < k1 then (k2, c2)::(add ((k1,c1)::p1) p2) else let val c = c1+c2 in if c = 0 then add p1 p2 else (k1,c)::(add p1 p2) end
FP
8.39
FP
8.40