Example: Queues
Queue Signature
Queues and stacks are fairly similar interfaces. Here, for sake of
example, we do something different with the interface that has nothing
to do with the data structure is a stack or a queue: we choose to
return an option
from some functions instead of raising an exception.
We could go back to our stack signature and re-code it in a similar way.
Which way to code it is in part a matter of taste:
The way we do it here, with options, ensures that surprising exceptions regarding empty queues never occur at run-time. The program is therefore more robust.
The way we did it before, with exceptions, means that programmers don't have to write as much code. If they are sure that an exception can't occur, they can omit the code for handling it.
There is thus a tradeoff between writing more code early (with options) or doing more debugging later (with exceptions).
module type Queue = sig
(* An ['a queue] is a queue whose elements have type ['a]. *)
type 'a queue
(* The empty queue. *)
val empty : 'a queue
(* Whether a queue is empty. *)
val is_empty : 'a queue -> bool
(* [enqueue x q] is the queue [q] with [x] added to the end. *)
val enqueue : 'a -> 'a queue -> 'a queue
(* [peek q] is [Some x], where [x] is the element at the front of the queue,
or [None] if the queue is empty. *)
val peek : 'a queue -> 'a option
(* [dequeue q] is [Some q'], where [q'] is the queue containing all the elements
of [q] except the front of [q], or [None] if [q] is empty. *)
val dequeue : 'a queue -> 'a queue option
end
Queue Implemented as a List
Here is an implementation of a functional queue data structure as a list:
module ListQueue : Queue = struct
(* Represent a queue as a list. The list [x1; x2; ...; xn] represents
the queue with [x1] at its front, followed by [x2], ..., followed
by [xn]. *)
type 'a queue = 'a list
let empty = []
let is_empty q = q = []
let enqueue x q = q @ [x]
let peek = function
| [] -> None
| x::_ -> Some x
let dequeue = function
| [] -> None
| _::q -> Some q
end
Dequeueing is constant-time with this representation, but enqueueing is
a linear-time operation. That's because dequeue
does a single pattern
match, whereas enqueue
must traverse the entire list to put the new
element at the end.
Queue Implemented with Two Lists
Here is a second, more efficient implementation of the Queue interface, using two lists to represent a single queue. This representation was invented by Robert Melville as part of his PhD dissertation at Cornell (Asymptotic Complexity of Iterative Computations, Jan 1981), which was advised by Prof. David Gries.
module TwoListQueue : Queue = struct
(* [{front=[a;b]; back=[e;d;c]}] represents the queue
containing the elements a,b,c,d,e. That is, the
back of the queue is stored in reverse order.
[{front; back}] is in *normal form* if [front]
being empty implies [back] is also empty.
All queues passed into or out of the module
must be in normal form. *)
type 'a queue = {front:'a list; back:'a list}
let empty = {front=[]; back=[]}
let is_empty = function
| {front=[]; back=[]} -> true
| _ -> false
(* Helper function to ensure that a queue is in normal form. *)
let norm = function
| {front=[]; back} -> {front=List.rev back; back=[]}
| q -> q
let enqueue x q = norm {q with back=x::q.back}
let peek = function
| {front=[]; _} -> None
| {front=x::_; _} -> Some x
let dequeue = function
| {front=[]; _} -> None
| {front=_::xs; back} -> Some (norm {front=xs; back})
end
With two-list queues, we now get a constant time enqueue
operation:
just cons a new element onto back
. But dequeue
is no longer just a
simple pattern match: it has to call norm
to ensure the queue it
returns is in normal form. So it might seem as though dequeue
no
longer has constant time efficiency. Nonetheless, with an advanced
algorithmic analysis technique (not covered here) called amortized
analysis, it is possible to conclude that this implementation of
dequeue
is essentially constant time.
That efficiency comes at a price in readability, though. If we compare
ListQueue
and TwoListQueue
, it's hopefully clear that ListQueue
is
a simple and correct implementation of a queue data structure. It's
probably far less clear that TwoListQueue
is a correct implementation!
Some of the exercises at the end of this chapter ask you to explore the efficiencies of these two implementations a bit further.