8.2 Queues from Lists
Suppose you have a list. When you take its first element, you get the
element that was most recently link
ed to create it. The next element is the
second most recent one that was link
ed, and so on. That is, the last one in is
the first one out. This is called a LIFO, short for “last-in-first-out”, data
structure. A list is LIFO; we sometimes also refer to this as a stack.
But there are many settings where you want the first-in to be the first-out. When you stand at a supermarket line, try to purchase concert tickets, submit a job request, or any number of other tasks, you want to be rewarded, not punished, for being there first. That is, you want a FIFO instead. This is called a queue.
The game we’re playing here is that we want one datatype but our language has given us another (in this case, lists), and we have to figure out how to encode one in the other. We’ll see elsewhere how to encode sets with lists [Representing Sets as Lists]. Here let’s see how we can encode queues with lists.
With sets, we allowed the set type to be an alias for lists; that is, the two were the same. Another option we have when encoding is to create a completely new type that does nothing more than wrap a value of the encoding type. We’ll use that principle here to illustrate how that might work.
8.2.1 Using a Wrapper Datatype
data Queue<T>:
| queue(l :: List<T>)
end
fun mk-mtq<T>() -> Queue<T>:
queue(empty)
end
fun is-mtq<T>(q :: Queue<T>) -> Boolean:
is-empty(q.l)
end
enqueue :: <T> Queue<T>, T -> Queue<T>
fun enqueue(q, e):
queue(link(e, q.l))
end
Do Now!
Did we have a choice?
Yes, we did! We could have made the new element the first element or the last element.Be careful here: we mean the first or last element of the list that represents the queue, not of the queue itself. There, FIFO gives us no choice. We just happened to choose one representation. The other would be equally valid; we would just need to implement all the other operations consistently. Let’s stick to this one for now.
qpeek :: <T> Queue<T> -> T
qrest :: <T> Queue<T> -> Queue<T>
q_ = mk-mtq()
q3 = enqueue(q_, 3)
m43 = enqueue(q3, 4)
m543 = enqueue(m43, 5)
check:
qpeek(q3) is 3
qpeek(m43) is 3
qpeek(m543) is 3
end
check:
qrest(q3) is mk-mtq()
qrest(m43) is enqueue(mk-mtq(), 4)
qrest(m543) is enqueue(enqueue(mk-mtq(), 4), 5)
end
fun qpeek(q):
if is-mtq(q):
raise("can't peek an empty queue")
else:
q.l.get(q.l.length() - 1)
end
end
fun qrest(q):
fun safe-rest(l :: List<T>) -> List<T>:
cases (List) l:
| empty => raise("can't dequeue an empty queue")
| link(f, r) => r
end
end
queue(safe-rest(q.l.reverse()).reverse())
end
8.2.2 Combining Answers
qpeek
and qrest
above have the
possibility of not having any more elements! We might as well reflect that too
in the type. Thus we end up with a type that looks like
data Dequeued<T>:
| none-left
| elt-and-q(e :: T, q :: Queue<T>)
end
Exercise
Write out the function to use this return type.
Observe that this also follows our principle of making exceptional behavior manifest in the return type: The Option Type, and especially in Summary.
Exercise
Write out the function using this return type.
8.2.3 Using a Picker
Does Dequeued
look familiar? Of course it should! It’s basically the
same as the pickers used for sets in Pyret: Picking Elements from Sets. If we make
queues provide the same operations, we can reuse the Pick
library
already built into the language, and reuse any code that is written expecting
the picker interface.
include pick
dequeue :: <T> Queue<T> -> Pick<T, Queue<T>>
check:
dequeue(q_) is pick-none
dequeue(q3) is pick-some(3, mk-mtq())
dequeue(m43) is pick-some(3, enqueue(mk-mtq(), 4))
dequeue(m543) is pick-some(3, enqueue(enqueue(mk-mtq(), 4), 5))
end
fun dequeue<T>(q):
rev = q.l.reverse()
cases (List) rev:
| empty => pick-none
| link(f, r) =>
pick-some(f, queue(r.reverse()))
end
end
qrest
or dequeue
. To see how to do
better, and to conduct a more sophisticated analysis, see
An Example: Queues from Lists.One thing to note is that by providing only a picker interface, we’re slightly changing the meaning of queues. The picker interface in Pyret is designed for sets, which don’t have a notion of order. But queues are, of course, very much an ordered datatype; order is why they exist. So by providing only a picker interface, we don’t offer the very guarantee that queues are designed for. Therefore, we should provide a picker in addition to an ordered interface, rather than in place of one.
At this point we’re done with the essential content, but here are two more parts that you may find interesting.
8.2.4 Using Tuples
Earlier, we created the Dequeued
datatype to represent the return value
from the dequeue. Indeed, it is often useful to create datatypes of this sort
to document functions and make sure the types can be meaningfully interpreted
even when their values flow around the code some distance from where they were
created.
Sometimes, however, we want to create a compound datum in a special circumstance: it represents the return value of a function, and that return value will not live for very long, i.e., it will be taken apart as soon as it has returned and only the constituent parts will be used thereafter. In such situations, it can feel like a burden to create a new datatype for such a fleeting purpose. For such cases, Pyret has a built-in generic datatype called the tuple.
;
) takes an expression, not only a
constant value:
{1; 2}
{3; 4; 5}
{1 + 2; 3}
{6}
{}
{a; b} = {1; 2}
a
and b
and see what they are bound to.
{c; d; e} = {1 + 2; 6 - 2; 5}
c
, d
, and e
are bound to.Exercise
What happens if we use too few or too many variables? Try out the following in Pyret and see what happens:{p; q} = {1} {p} = {1; 2} {p} = 1
Do Now!
What happens if instead we write this?p = {1; 2}
This binds p
to the entire tuple.
Exercise
How might we pull apart the constituents of
p
?
fun dequeue-tuple<T>(q :: Queue<T>) -> {T; Queue<T>}:
rev = q.l.reverse()
cases (List) rev:
| empty => raise("can't dequeue an empty queue")
| link(f, r) =>
{f; queue(r.reverse())}
end
end
check:
dequeue-tuple(q3) is {3; mk-mtq()}
dequeue-tuple(m43) is {3; enqueue(mk-mtq(), 4)}
dequeue-tuple(m543) is {3; enqueue(enqueue(mk-mtq(), 4), 5)}
end
fun q2l<T>(q :: Queue<T>) -> List<T>:
if is-mtq(q):
empty
else:
{e; rq} = dequeue-tuple(q)
link(e, q2l(rq))
end
end
check:
q2l(mk-mtq()) is empty
q2l(q3) is [list: 3]
q2l(m43) is [list: 3, 4]
q2l(m543) is [list: 3, 4, 5]
end
8.2.5 A Picker Method
Set
s had a
built-in pick
method. We have a function, but not method,
that picks. Now we’ll see how we can write this as a method:
data Queue<T>:
| queue(l :: List<T>) with:
method pick(self):
rev = self.l.reverse()
cases (List) rev:
| empty => pick-none
| link(f, r) =>
pick-some(f, queue(r.reverse()))
end
end
end
Queue
,
because we’ve added a method but left the general datatype structure intact, so
all our existing code will still work. In addition, we can rewrite q2l
in terms
of the picker interface:
fun q2lm<T>(c :: Queue<T>) -> List<T>:
cases (Pick) c.pick():
| pick-none => empty
| pick-some(e, r) => link(e, q2lm(r))
end
end
check:
q2lm(m543)
end
fun pick2l<T>(c) -> List<T>:
cases (Pick) c.pick():
| pick-none => empty
| pick-some(e, r) => link(e, pick2l(r))
end
end
Queue
s:
import sets as S # put this at the top of the file
check:
pick2l([S.set: 3, 4, 5]).sort() is [list: 3, 4, 5]
pick2l(m543) is [list: 3, 4, 5]
end
Exercise
Do you see why we invoked
sort
in the test above?
The only weakness here is that for this last part (making the function
generic), we have to transition out of the type-checker, because pick2l
cannot be typed by the current Pyret type checker. It requires a feature that
the type checker does not (yet) have.