Administrivia: PS#3 out, pick a partner (or not) by tomorrow, Friday 11:59PM.
Last time: precise evaluation model and rewrite rule
This time: induction, program correctness, intro to stacks and queues.
We have now a precise semantics for our ML subset, we can use our semantic in different areas. In particular we use it to prove properties of programs, i.e. correctness and efficiency.
There are 2 key tools for this. The first one is our precise semantics; without this, we have no definition of what an ML program does, hence no hope of proving anything about it. Our semantics is precise enough so that one can prove things with arbitrary detail; it�s good to know how to do this, but you won�t be required to do this in the course.
The second key tool is, of course, induction. Almost all proofs in CS are based on induction.
We are going to prove a mathematical property of a program, with respect to a semantics. For example we might prove that fact(n) = n!. To say this with complete precision, what we are proving is:
For all integers n > 0,
eval((fun fact(z:int):int = if (z = 1) then z else z * fact (z - 1)) (n)) = n!This is, to be precise, an infinite set of statements, one for each value of n. Above, we started to prove it for n = 3. Not much fun for n = 106, etc�
We will prove this statement by induction. This means that for all n � Z+, we will prove that fact(n) = n! We�ll do this just once, rather than �0 times.
An induction proof has 4 parts:
We clearly state each the four steps
eval((fun fact(z:int):int = if (z = 1) then z else z * fact (z - 1)) (1)) � eval((fn (z:int):int => if (z = 1) then z else z * (fun fact(z:int):int = if (z = 1) then z else z * fact (z � 1)) (z � 1)) (1)) � eval(if (1 = 1) then 1 else 1 * (fun fact(z:int):int = if (z = 1) then z else z * fact (z � 1)) (1 - 1))) � 1Then we will continue proving the inductive step:
eval((fun fact(z:int):int = if (z = 1) then z else z * fact (z - 1)) (m+1)) � eval((fn (z:int):int => if (z = 1) then z else z * (fun fact(z:int):int = if (z = 1) then z else z * fact (z � 1)) (z � 1)) (m+1)) � eval(if (m+1 = 1) then m+1 else (m+1) * (fun fact(z:int):int = if (z = 1) then z else z * fact (z � 1)) (m + 1 - 1))) �We know that m � Z+, then m >= 1 and m+1>1, therefore the if statemante results false and will evaluate the second expression.
eval((m+1) * (fun fact(z:int):int = if (z = 1) then z else z * fact (z � 1)) (m)) � eval(eval(m+1) * eval((fun fact(z:int):int = if (z = 1) then z else z * fact (z � 1)) (m)))Now we apply our Induction Hypothesis.
� eval((m+1) * m!) � (m+1)!
Next we will turn to more examples of structures and signatures, to implement data structures. We concentrate on (functional) stacks and queues, and also talk about invariants.
A functional stack, or a functional queue, it is a data structure for which the operations do not change the data structure, but rather create a new data structure, with the appropriate modifications, instead of changing it in-place.
Recall a stack: a last-in first-out data structure. Just like lists, the stack operations fundamentally do not care about the type of the values stored, so it is a naturally polymorphic data structure.
Here is a possible signature for functional stacks:
signature STACK = sig type 'a stack exception EmptyStack val empty : 'a stack val isEmpty : 'a stack -> bool val push : ('a * 'a stack) -> 'a stack val pop : 'a stack -> 'a stack val top : 'a stack -> 'a val map : ('a -> 'b) -> 'a stack -> 'b stack endThis signature specifies a parameterized abstract type for stack. Notice the type variable 'a. The signature also specifies the empty stack value, and functions to check if a stack is empty, and to perform push, pop and top operations on the stack. Moreover, we specify a function map to walk over the values of the stack. We also declare an exception EmptyStack to be raised by top and pop operations when the stack is empty. Note also that there are some interesting mathematical invariants that cannot be captured in the signature. For example: pop(push(x,S)) = S, top(push(x,S)) = x. Does this look familiar? These properties can't be written down as part of ML, but only in the comments. Sometimes one can make a formal spec for a data structure; this is particularly true of functional data structures. Here is the simplest implementation of stacks that matches the above signature. It is implemented in terms of lists.
structure Stack :> STACK = struct type 'a stack = 'a list exception Empty val empty : 'a stack = [] fun isEmpty (l:'a list): bool = (case l of [] => true | _ => false) fun push (x:'a, l:'a stack):'a stack = x::l fun pop (l:'a stack):'a stack = (case l of [] => raise Empty | (x::xs) => xs) fun top (l:'a stack):'a = (case l of [] => raise Empty | (x::xs) => x) fun map (f:'a -> 'b) (l:'a stack):'b stack = List.map f l end
Up until now, we have been defining exceptions solely in order to raise them and interrupt the executing program. Just like in Java, it is also possible to catch exceptions, which is termed 'handling an exception' in SML.
As an example, consider the following example. In the above code, we have
implemented top and pop respectively as functions that return the first element
of the list and the rest of the list. SML already defines functions to do just
that, namely hd
and tl
(for head and tail). The
function hd takes a list as argument and returns the first element of the list,
or raises the exception Empty
if the list is empty. Similarly for tl
.
One would like to simply be able to write in Stack
: fun top (l:'a
stack):'a = hd (l) fun pop (l:'a stack):'a stack = tl (l)
However, if passed an empty stack, top and pop should raise the EmptyStack exception. As written above, the exception List.Empty would be raised. What we need to do is intercept (or handle) the exception, and raise the right one. Here's one way to do it:
fun top (l:'a stack):'a = hd (l) handle List.Empty => raise EmptyStack fun pop (l:'a stack):'a stack = tl (l) handle List.Empty => raise EmptyStack
The syntax for handling exceptions is as follows: e handle exn
=> e'
where e is the expression to evaluate, and if e raises an exception that matches exn, then expression e' is evaluated instead. The type of e and e' must be the same.
Let us write an example more interesting than stacks. After all, from the above, one can see that they are just lists. Consider the queue data structure, a first-in first-out data structure. Again, we consider functional queues. Here is a possible signature:
signature QUEUE = sig type 'a queue exception EmptyQueue val empty : 'a queue val isEmpty : 'a queue -> bool val enqueue : ('a * 'a queue) -> 'a queue val dequeue : 'a queue -> 'a queue val front : 'a queue -> 'a val map : ('a -> 'b) -> 'a queue -> 'b queue val app : ('a -> unit) -> 'a queue -> unit end
The simplest possible implementation for queues is to represent a queue via two stacks: one stack A on which to enqueue elements, and one stack B from which to dequeue elements. When dequeuing, if stack B is empty, then we reverse stack A and consider it the new stack B.
Here is an implementation for such queues. It uses the stack structure Stack, which is rebound to the name S inside the structure to avoid long identifier names.
structure Queue :> QUEUE = struct structure S = Stack type 'a queue = ('a S.stack * 'a S.stack) exception EmptyQueue val empty : 'a queue = (S.empty, S.empty) fun isEmpty ((s1,s2):'a queue) = S.isEmpty (s1) andalso S.isEmpty (s2) fun enqueue (x:'a, (s1,s2):'a queue) : 'a queue = (S.push (x,s1), s2) fun rev (s:'a S.stack):'a S.stack = let fun loop (old:'a S.stack, new:'a S.stack):'a S.stack = if (S.isEmpty (old)) then new else loop (S.pop (old), S.push (S.top (old),new)) in loop (s,S.empty) end fun dequeue ((s1,s2):'a queue) : 'a queue = if (S.isEmpty (s2)) then (S.empty, S.pop (rev (s1))) handle S.EmptyStack => raise EmptyQueue else (s1,S.pop (s2)) fun front ((s1,s2):'a queue):'a = if (S.isEmpty (s2)) then S.top (rev (s1)) handle S.EmptyStack => raise EmptyQueue else S.top (s2) fun map (f:'a -> 'b) ((s1,s2):'a queue):'b queue = (S.map f s1, S.map f s2) fun app (f:'a -> unit) ((s1,s2):'a queue):unit = (S.app f s2; S.app f (rev (s1))) end