CS312 Lecture 9: Induction; Stacks and Queues

Overview

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.

Use of the Semantics

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.

Induction summary (for proofs of program correctness).

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:

  1. A precise statement of what we are proving (given above, in our example). We may call this statement P[n]; it is a mathematical statement about n.
  2. What set N we are doing induction over, and what variable we are using to represent an element of that set. Often, there is more than one possibility; here, only N = Z+, induction on n.
  3. A proof for the base case, i.e. P[z] where z is the smallest element of N. Typically z = 1 (sometimes z = 0). More precisely, a proof of P[1] in our example.
  4. A proof that for any m Î N, P[m] Þ P[m+1] (weak in induction). Note that this is not the same as ("m Î N, P[m]) Þ ("m Î N, P[m+1]), which is true for any P on any infinite set. Instead we pick an arbitrary m, assume P[m] is true (Induction Hypothesis) and prove P[m+1]

Example: fact(n) = n!

We clearly state each the four steps

  1. fact(n) = n!
  2. Induction on n in Z+
  3. We need to show fact(1) = 1. See below.
  4. We need to show that for any m in Z+, fact(m) = m! => fact(m+1) = (m+1)! See below.
Then we do the base case.
  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)))

Þ

  1
Then 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)!

Functional Data Structures

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
    end
This 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