Definition of a New Collection¶
In this chapter, we implement a data structure called a sorted sequence. A sorted sequence is a sequence that automatically keeps the elements of the sequence in a particular order, based on some value computed from each element. Elements are added and removed from sorted sequences; however, the sorted sequence determines the key associated with the element. Thus, it does not make sense to store an element in a sorted sequence at a specific key, because the sorted sequence will determine the correct key to satisfy the automatic-ordering constraint.
We use Dylan’s forward-iteration-protocol
to implement the connection
between our new collection class and Dylan’s standard collection generic
functions. Dylan’s forward-iteration protocol is a well-defined
interface that collection implementors and collection-iterator
implementors can use to enable iterators to operate over new
collections, and to enable collections to work with new iterators. Once
the forward iteration protocol is defined on <sorted-sequence>
, many
of the standard Dylan collection generic functions that we covered in
Collections and Control Flow, will work with instances of the new sequence.
The airport application uses a sorted sequence to keep track of aircraft transition in time order. See The Airport Application, for more details.
The sorted-sequence.dylan
file¶
The sorted-sequence.dylan
file contains the module constants, classes,
and methods that build on Dylan’s collection framework to define the
structure and behavior of the new <sorted-sequence>
collection.
A new collection class¶
The sorted-sequence.dylan
file.
module: sorted-sequence
define class <sorted-sequence> (<sequence>)
// The vector that stores the elements of the sorted sequence, in order
slot data :: <stretchy-vector> = make(<stretchy-vector>, size: 0);
// The function used to extract the comparison value from an element
constant slot value-function :: <function> = identity,
init-keyword: value-function:;
// The function used to determine whether one comparison value is
// smaller than another comparison value
constant slot comparison-function :: <function> = \<,
init-keyword: comparison-function:;
end class <sorted-sequence>;
Because is there is a well-defined ordering of the elements of sorted
sequences, we choose <sequence>
to be the superclass of
<sorted-sequence>
. We use the built-in collection class called
<stretchy-vector>
to store the elements of our sorted sequence,
because we want to be able to have the sorted sequence grow to any size
in a convenient way.
The slots comparison-function
and value-function
are constant slots,
because we intend to have clients specify these functions only when they
create the sorted sequence. If we had decided to let clients change the
value of these slots, we would have made the slots virtual, so that we
could reorder the data vector after either function had changed.
Now that we have covered the structure and initialization of the sorted sequence data structure, we can define basic collection methods.
Basic collection methods¶
The sorted-sequence.dylan
file. (continued)
define method size (sorted-sequence :: <sorted-sequence>)
=> (sorted-sequence-size :: <integer>)
sorted-sequence.data.size;
end method size;
define method shallow-copy (sorted-sequence :: <sorted-sequence>)
=> (copy :: <sorted-sequence>)
let copy
= make(<sorted-sequence>,
value-function: sorted-sequence.value-function,
comparison-function: sorted-sequence.comparison-function);
// The map-into function replaces the elements of the copy’s data array
// to be the identical elements of the data array of sorted sequence
copy.data.size := sorted-sequence.data.size;
map-into(copy.data, identity, sorted-sequence.data);
copy;
end method shallow-copy;
define constant $unsupplied = list(#f);
define method element
(sorted-sequence :: <sorted-sequence>, key :: <integer>,
#key default = $unsupplied)
=> (element :: <object>);
if (key < sorted-sequence.data.size)
sorted-sequence.data[key];
elseif (default = $unsupplied)
error("Attempt to access key %= which is outside of %=.", key,
sorted-sequence);
else default;
end if;
end method element;
In the preceding code, we define methods for determining the number of
elements in the sorted sequence, for copying the sorted sequence (but
not the elements stored in the sorted sequence), and for accessing a
particular item in the sorted sequence. Once we have defined the
element
method for sorted sequences, we can use the subscripting
syntax to access particular items in the sorted sequence. Our element
method implements the standard Dylan protocol, which allows the caller
to specify a default value if the key is not contained within the
collection. If the key is not part of the collection, and no default
value is specified, then an error is signaled. Since we do not export
$unsupplied
from our library, we can be certain that no one can supply
that value as the default
keyword parameter for our element
method.
Note that the element-setter
method is not defined, because it does
not make sense to store an element at a particular position within the
sorted sequence. The sorted sequence itself determines the correct key
for each item added to the sorted sequence, based on the item being
added and on the value and comparison functions.
Next, we show methods for adding and removing elements from sorted sequences.
Adding and removing elements¶
The sorted-sequence.dylan
file. (continued)
// Add an element to the sorted sequence
define method add!
(sorted-sequence :: <sorted-sequence>, new-element :: <object>)
=> (sorted-sequence :: <sorted-sequence>)
let element-value = sorted-sequence.value-function;
let compare = sorted-sequence.comparison-function;
add!(sorted-sequence.data, new-element);
sorted-sequence.data
:= sort!(sorted-sequence.data,
test: method (e1, e2)
compare(element-value(e1), element-value(e2))
end);
sorted-sequence;
end method add!;
// Remove the item at the top of the sorted sequence
define method pop (sorted-sequence :: <sorted-sequence>)
=> (top-of-sorted-sequence :: <object>)
let data-vector = sorted-sequence.data;
let top-of-sorted-sequence = data-vector[0];
let sorted-sequence-size = data-vector.size;
if (empty?(sorted-sequence))
error("Trying to pop empty sorted-sequence %=.", sorted-sequence);
else
// Shuffle up existing data, removing the top element from the
// sorted sequence
for (i from 0 below sorted-sequence-size - 1)
data-vector[i] := data-vector[i + 1];
end for;
// Decrease the size of the data vector, and return the top element
data-vector.size := sorted-sequence-size - 1;
top-of-sorted-sequence;
end if;
end method pop;
// Remove a particular element from the sorted sequence
define method remove!
(sorted-sequence :: <sorted-sequence>, value :: <object>,
#key test = \==, count = #f)
=> (sorted-sequence :: <sorted-sequence>)
let data-vector = sorted-sequence.data;
let sorted-sequence-size = data-vector.size;
for (deletion-point from 0,
// If we have reached the end of the sequence, or we have reached
// the user-specified limit, we are done
// Note that specifying a bound in the preceding clause for
// deletion-point does not work, because bounds are computed only
// once, and we change sorted-sequence-size in the body
until: (deletion-point >= sorted-sequence-size)
| (count & count = 0))
// Otherwise, if we found a matching element, remove it from the
// sorted sequence.
if (test(data-vector[deletion-point], value))
for (i from deletion-point below sorted-sequence-size - 1)
data-vector[i] := data-vector[i + 1]
end for;
sorted-sequence-size
:= (data-vector.size := sorted-sequence-size - 1);
if (count) count := count - 1 end;
end if;
end for;
sorted-sequence;
end method remove!;
The remove!
method uses a form of the for
loop that includes an
until:
clause, much like the my-copy-sequence
method defined in
Lists and efficiency. Note that all termination checks are tested
prior to the execution of the body.
Although the pop
method is not used in the airport application, it is
included for completeness. We could make the pop
method faster by
storing the data elements in reverse order; however, that would lead to
either odd behavior or odd implementation of the element
function on
sorted sequences.
The forward-iteration protocol¶
Dylan’s forward-iteration protocol allows us to connect the usual
collection iteration functions to our new collection class. Connecting
to the forward-iteration protocol is as simple as defining an
appropriate method for the forward-iteration-protocol
generic
function. This method must return two objects and six functions.
The sorted-sequence.dylan
file. (continued)
// This method enables many standard and user-defined collection operations
define method forward-iteration-protocol
(sorted-sequence :: <sorted-sequence>)
=> (initial-state :: <integer>, limit :: <integer>,
next-state :: <function>, finished-state? :: <function>,
current-key :: <function>, current-element :: <function>,
current-element-setter :: <function>, copy-state :: <function>)
values(
// Initial state
0,
// Limit
sorted-sequence.size,
// Next state
method (collection :: <sorted-sequence>, state :: <integer>)
state + 1
end,
// Finished state?
method (collection :: <sorted-sequence>, state :: <integer>,
limit :: <integer>)
state = limit;
end,
// Current key
method (collection :: <sorted-sequence>, state :: <integer>)
state
end,
// Current element
element,
// Current element setter
method (value :: <object>, collection :: <sorted-sequence>,
state :: <integer>)
error("Setting an element of a sorted sequence is not allowed.");
end,
// Copy state
identity);
end method forward-iteration-protocol;
If we are to iterate over any collection, we must maintain some state to
help the iterator remember the current point of iteration. For the
forward-iteration protocol, we maintain this state using any object
suitable for a given collection. In this case, an integer is sufficient
to maintain where we are in the iteration process. The first object
returned by forward-iteration-protocol
is a state object that is
suitable for the start of an iteration. The second object returned is a
state object that represents the ending state of the iteration. Since,
in this case, the state object is just the current key of the sorted
sequence, the integer 0 is the correct initial state, and the integer
that represents the size of the collection is the correct ending state.
The third value returned is a function that takes the collection and the current iteration state, and returns a state that is the next step in the iteration. In this case, we can determine the next state simply by adding 1 to the current state.
The fourth value returned is a function that receives the collection, the current state, and the ending state, and that determines whether the iteration is complete. In this case, we need only to check whether the current state is equal to the ending state.
The fifth value returned is a function that generates the current key into the collection, given a collection and a state. In this case, the key is the state object.
The sixth value returned is a function that receives a collection and a
state, and returns the current element of the collection. In this case,
the element
function is the obvious choice, since our state is just
the key.
The seventh value returned is a function that receives a new value, a
collection, and a state, and changes the current element to be the new
value. In this case, such an operation is illegal, since the only
rational way to add elements to sorted sequences is with add!
.
Because this operation is illegal, an error is signaled.
The eighth and final value returned is a function that receives a collection and a state, and returns a copy of the state. In this case, we just return the state, because it is an integer and thus has no slots that are modified during the iteration process. If we represented the state with an object that had one or more slots that did change during iteration, we would have to make a new state instance and to copy the significant information from the old state instance to the new state instance.
Once we have defined a forward-iteration-protocol
method for sorted
sequences, we can iterate over them using for
loops, mapping
functions, and other collections iterators described in Collections and Control Flow.
Also, if someone defines a new iterator that uses the forward-iteration
protocol, then this new iterator will work with sorted sequences.
Dylan has several other related protocols for backward iteration and for tables. See the The Dylan Reference Manual for details.
The sorted-sequence-library.dylan
file¶
The definitions for the sorted sequence library and module are simple.
The only module variable that we need to export is for the sorted
sequence class itself. All the generic functions that we want clients to
use on sorted sequences are exported by the dylan
module.
The sorted-sequence-library.dylan
file.
module: dylan-user
define library sorted-sequence
export sorted-sequence;
use dylan;
use definitions;
end library sorted-sequence;
define module sorted-sequence
export <sorted-sequence>;
use dylan;
use definitions;
end module sorted-sequence;
The definitions
library and module are defined in The Airport Application.
The sorted-sequence.lid
file¶
The LID file for sorted sequences is also straightforward. The entire
library is contained within two files (in addition to the LID file
itself). The library and module definitions are in the file
sorted-sequence-library.dylan
. The definitions of module constants,
classes, and methods are in the implementation file,
sorted-sequence.dylan
.
The sorted-sequence.lid
file.
library: sorted-sequence
files: sorted-sequence-library
sorted-sequence
Summary¶
In this chapter, we covered the following:
We explored how to define our own collection class.
We showed how to integrate that class into Dylan’s collection framework.
We used several variations of the control structures presented in Collections and Control Flow.