Example: Stacks
Here are a few familiar operations on stacks along with their types.
module type Stack = sig
type 'a t
val empty : 'a t
val is_empty : 'a t -> bool
val peek : 'a t -> 'a
val push : 'a -> 'a t -> 'a t
val pop : 'a t -> 'a t
end
As usual, there is a design choice to be made with peek
etc. about what to do
with empty stacks. Here we have not used option
, which suggests that peek
will raise an exception on the empty stack. So we are cautiously relaxing
our prohibition on exceptions.
In the past we've given these operations specifications in English, e.g.,
(* [push x s] is the stack [s] with [x] pushed on the top *)
val push : 'a -> 'a stack -> 'a stack
But now, we'll instead write some equations to describe how the operations work:
1. is_empty empty = true
2. is_empty (push x s) = false
3. peek (push x s) = x
4. pop (push x s) = s
(Later we'll return to the question of how to design such equations.) The variables appearing in these equations are implicitly universally quantified. Here's how to read each equation:
is_empty empty = true
. The empty stack is empty.is_empty (push x s) = false
. A stack that has just been pushed is non-empty.peek (push x s) = x
. Pushing then immediately peeking yields whatever value was pushed.pop (push x s) = s
. Pushing then immediately popping yields the original stack.
Just with these equations alone, we already can infer a lot about how any sequence of stack operations must work. For example,
peek (pop (push 1 (push 2 empty)))
= { equation 4 }
peek (push 2 empty)
= { equation 3 }
2
And peek empty
doesn't equal any value according to the equations, since there
is no equation of the form peek empty = ...
. All that is true regardless of
the stack implementation that is chosen: any correct implementation must cause
the equations to hold.
Suppose we implemented stacks as lists, as follows:
module ListStack = struct
type 'a t = 'a list
let empty = []
let is_empty s = (s = [])
let peek = List.hd
let push = List.cons
let pop = List.tl
end
Next we could prove that each equation holds of the implementation. All these proofs are quite easy by now, and proceed entirely by evaluation. For example, here's a proof of equation 3:
peek (push x s)
= { evaluation }
peek (x :: s)
= { evaluation }
x