The classes of the Iteration cluster encapsulate control structures representing common traversal operations.
The recent introduction of the agents mechanism in ISE Eiffel offers an attractive alternative to the Iterator cluster of EiffelBase.
Let us first explore the role of iterators in the architecture of a system.
Client software that uses data structures of a certain type, for example lists or trees,
often needs to traverse a data structure of that type in a predetermined order so as to
apply a certain action to all the items of the structure, or to all items that satisfy a certain
criterion. Such a systematic traversal is called an iteration.
Cases of iteration can be found in almost any system. Here are a few typical
examples:
These examples illustrate the general properties of iteration. An iteration involves a data structure of a known general type and a particular ordering of the structure’s items. For some structures, more than one ordering will be available; for example a tree iteration may use preorder, postorder or breadth-first (as defined below). The iteration involves an operation, say item_action, to be applied to the selected items. It may also involve a boolean-valued query, say item_test, applicable to candidate items. Finally, it involves a certain policy, usually based on item_test, as to which items should be subjected to item_action. Typical example policies are:
The Iteration library provides many more, covering in particular all the standard control structures.
You can perform iterations without any special iteration classes. For example if customers is declared as
customers: LIST [CUSTOMER]
then a class SPECIAL_PROMOTION of a text processing system may include in one of its routines a loop of the form
from
customers.start
until
customers.exhausted
loop
if
recent_ purchases.has
(customers.item)
then
target_list.put
(customers.item)
end
customers.forth
end
Such schemes are quite common. But it is precisely because they occur frequently
that it is useful to rely on library classes to handle them. One of the principal tasks of
object-oriented software development is to identify recurring patterns and build reusable
classes that encapsulate them, so that future developers will be able to rely on ready-made
solutions.
The classes of the Iteration library address this need. Using them offers two
benefits:
To get a first grasp of how one can work with the Iteration library, let us look at a typical iteration class and a typical iteration client.
Here, given with its full implementation, is a typical Iteration library routine: the procedure until_do from LINEAR_ITERATOR, the class defining iteration mechanisms on linear (sequential) structures.
until_do
is
-- Apply action to every item of target,
-- up to but excluding first one satisfying test.
-- (Apply to full list if no item satisfies test.)
require
traversable_exists:
target /=
Void
do
from
target.start
invariant
invariant_value
until
target.exhausted
or else
test
loop
action
target.forth
end
ensure
achieved:
target.exhausted
or else test
invariant_satisfied:
invariant_value
end
The precise form of the procedure in the class relies on a call to another procedure,
until_continue,
and on inherited assertions. Here everything has been unfolded for illustration
purposes.
This procedure will traverse the linear structure identified by
target
and apply the procedure called
action
on every item up to but excluding the first one satisfying test.
The class similarly offers
do_all,
do_while,
do_for,
do_if
and other procedures representing the common control structures.
It also includes functions such as
exists and
forall,
corresponding to the usual quantifiers.
These iteration schemes depend on the procedure
action,
defining the action to be applied to successive elements, and on the function
test,
defining the boolean query to be applied to these elements. These features are declared in class
ITERATOR
(the highest-level deferred class of the Iteration library); here is
test:
test:
BOOLEAN is
-- Test to be applied to
item
at current position in
--
target
(default: value of
item_test
on item)
require
traversable_exists:
target /=
Void
not_off:
not
target.off
do
Result
:= item_test
(target.item)
ensure
not_off:
not
target.off
end
This indicates that the value of the boolean function test will be obtained by
applying item_test to the
item at the current position in the
target structure. In
ITERATOR, function
item_test always return
False; descendant classes will redefine it
so as to describe the desired test.
Similarly, action is declared in class ITERATOR as a
call to item_action.
Descendants will redefine
item_action,
which as initially declared in
ITERATOR
is a procedure with a null body.
Going through item_action
and item_test
provides an extra degree of flexibility. Normally the action and test performed at each step apply to
target.itemitem,
so that it suffices to redefine the
item_features.
This is the case will all examples studied in this chapter. In a more general
setting, however, you might need to redefine action and test themselves.
Here now is an example illustrating the use of these mechanisms. The result will enable us to resize all the paragraphs of a text up to the first one that has been modified - as we might need to do, in a text processing system, to process an interactive user request. Assume a class TEXT that describes lists of paragraphs with certain additional features. The example will also assume a class PARAGRAPH with a procedure resize, and a boolean-valued attribute modified which indicates whether a paragraph has been modified. Class TEXT inherits from LINKED_LIST and so is a descendant of LINEAR:
class
TEXT
inherit
LINKED_LIST
[PARAGRAPH]
...
feature
...
end
In a class TEXT_PROCESSOR, you can use an iteration procedure to write a very simple procedure resize_ paragraphs that will resize all paragraphs up to but excluding the first one that has been modified:
class
TEXT_PROCESSOR
inherit
LINEAR_ITERATOR
[PARAGRAPH]
redefine
item_action,
item_test
end
feature
resize_ paragraphs
(t:
TEXT) is
-- Resize all the paragraphs of t up to but excluding
-- the first one that has been modified.
do
set
(t)
until_do
end
feature
{NONE}
item_test
(p
PARAGRAPH):
BOOLEAN
is
-- Has
p been modified?
do
Result
:=
p.modified
end
item_action
(p:
PARAGRAPH)
is
-- Resize
p.
do
p.resize
end
...
end
Thanks to the iteration mechanism, the procedure resize_ paragraphs
simply needs two procedure calls:
Procedure
item_action
is redefined to describe the operation to be performed on each successive element. Function
item_test
is redefined to describe the exit test.
As presented so far, the mechanism seems to limit every descendant of an iteration class to just
one form of iteration. As shown later in this chapter, it is in fact easy to generalize the
technique to allow a class to use an arbitrary number of iteration schemes.
What is interesting here is that the redefinitions of
item_test
and
item_action
take care of all the details. There is no need to write any loop or other control structure. We
are at the very heart of the object-oriented method, enjoying the ability to encapsulate
useful and common software schemes so that client developers will only need to fill in
what is specific to their application.
Let us now explore the classes of the Iteration library and the different ways of using them.
There are only four Iteration classes, whose simple inheritance structure appeared at the beginning of this chapter.
As you will remember from the presentation of the abstract overall taxonomy,
the traversal hierarchy describes how data structures can be traversed; its most general class
is TRAVERSABLE.
Each one of the iterator classes is paired with a traversal class (or two in one case):
ITERATOR | TRAVERSABLE |
LINEAR_ITERATOR | LINEAR |
TWO_WAY_CHAIN_ITERATOR | TWO_WAY_LIST |
TWO_WAY_CHAIN_ITERATOR | TWO_WAY_LIST, TWO_WAY_CIRCULAR |
CURSOR_TREE_ITERATOR | CURSOR_TREE |
Class
ITERATOR
defines the features that apply to all forms of iterator.
An iterator will always apply to a certain target structure. The target is introduced in
ITERATOR by the feature
target:
TRAVERSABLE [G]
Both the iterator classes and the traversal classes are generic, with a formal generic
parameter G. The actual generic parameters will be matched through the choice of
iteration target: for a generic derivation of the form
SOME_ITERATOR [ACTUAL_TYPE]
the target can only be of type
SOME_TRAVERSABLE [ACTUAL_TYPE]
for the same ACTUAL_TYPE, where
SOME_TRAVERSABLE is the traversal class matching
SOME_ITERATOR according to the preceding table
(LINEAR
for
LINEAR_ITERATOR
and so on), or one of its proper descendants.
Each of the proper descendants of
ITERATOR
redefines the type of target to the matching proper descendant of
TRAVERSABLE,
to cover more specific variants of the iteration target, For example in
LINEAR_ITERATOR the feature is redefined to be of
type LINEAR.
ITERATOR
also introduces the procedure for selecting a target:
set
(s:
like
target)
is
-- Make
s the new target of iterations.
require
s
/= Void
do
target
:= s
ensure
target
= s
target
/= Void
end
Next ITERATOR introduces the routines describing the elementary action and test that will be applied to items of the iteration targets:
action
is
-- Action to be applied to item at current position in
-- target.
-- (default:
item_action
on
item
at current position.)
-- Note: for iterators to work properly, redefined
-- versions of this feature should not change the
-- traversable structure.
require
traversable_exists:
target
/= Void
not_off:
not
target.off
invariant_satisfied:
invariant_value
do
item_action
(target.item)
ensure
not_off:
not
target.off
invariant_satisfied:
invariant_value
end
test:
BOOLEAN
is
-- Test to be applied to
item
at current position in
-- target (default: value of
item_test
on
item)
require
traversable_exists:
target
/= Void
not_off:
not
target.off
do
Result :=
item_test
(target.item)
ensure
not
target.off
end
These routines rely on two others,
item_action and
item_test,
which both take an argument of type G, the formal generic parameter. The reason, already noted above, is
that in a vast majority of cases the iterated action and test solely depend, at each step of
the traversal, on the item (of type G) at the current position. To define an iteration
process, then, it suffices to redefine item_action and item_test in a descendant of the
appropriate iteration class. Only in complex cases will it be necessary to redefine action
and test themselves.
If you encounter such a case, note the caveat about action changing the target’s structure.
Understandably enough, an iterator that attempts to change the data structure while traversing it
may engage in strange behavior. No such risk exists if you only redefine item_action, which
may change the contents of items but not the structure itself.
Another feature introduced in
ITERATOR
is the query
invariant_value,
describing invariant properties that must be ensured at the beginning of any iteration and preserved
by every iteration step. As declared in
ITERATOR
this query always returns true, but proper descendants can redefine it to describe more interesting
invariant properties.
Finally,
ITERATOR
introduces in deferred form the general iteration routines applicable to all iteration variants.
They include two queries corresponding to the quantifiers of first-order predicate calculus:
The other routines are commands which will traverse the target structure and apply action to items selected through test:
All these features, and most of the other iteration features introduced in proper descendants of ITERATOR and described next, have no argument. Information about the target of iteration comes from feature target, set by procedure set; information about what needs to be done for each item of the target structure comes from item_action and item_test.
LINEAR_ITERATOR,
an effective class, refines the iteration mechanisms for cases in which the target is a linear
structure, such as a list in any implementation or a circular chain.
The class effects all the deferred features inherited from
ITERATOR,
taking advantage of the linear traversal mechanisms present in the corresponding traversal class,
LINEAR.
Here for example is the effecting of
do_if:
do_if
is
-- Apply action to every item of target satisfying
-- test.
do
from
target.start
invariant
invariant_value
until
target.exhausted
loop
if
test
then
action
end
forth
end
end
This routine text relies on features start, forth and exhausted which, together with off, have for convenience been carried over to LINEAR_ITERATOR from their counterparts in LINEAR, with feature declarations such as
off:
BOOLEAN is
-- Is position of target off?
require
traversable_exists:
target
/= Void
do
Result :=
target.off
end
and similarly for the others.
In addition to effecting the general iteration features from
ITERATOR,
class
LINEAR_ITERATOR
introduces iteration features that apply to the specific case of linear structures:
Class
TWO_WAY_CHAIN_ITERATOR
has all the features of
LINEAR_ITERATOR,
to which it adds features for iterating backward as well as forward.
The class introduces commands
finish and
back,
applying the corresponding operations to the two-way target. It also has a backward variant for every iteration
feature. The name of each such variant is the name of the forward feature followed by
_back:
do_all_back,
until_do_back
and so on.
An alternative design would have kept just one set of features and added two features: a
command reverse to reverse the direction of future iteration operations, and a query backward
to find out the direction currently in force.
Contrary to what one might at first imagine, class
TWO_WAY_CHAIN_ITERATOR
is extremely short and simple; its Feature clause only contains the declarations of two
features,
finish and
back.
The trick is to use repeated inheritance.
TWO_WAY_CHAIN_ITERATOR
inherits twice from
LINEAR_ITERATOR;
the first inheritance branch yields the forward iteration features,
the second yields those for backward iteration. There is
no need for any explicit declaration or redeclaration of iteration features. Here is the
entire class text that yields this result:
class
TWO_WAY_CHAIN_ITERATOR
[G]
inherit
LINEAR_ITERATOR
[G]
redefine
target
select
start,
forth,
do_all,
until_do,
do_until,
do_if,
do_for,
search,
forall,
exists,
until_continue,
continue_until,
continue_for,
continue_search
end
LINEAR_ITERATOR
[G]
rename
start
as
finish,
forth
as
back,
do_all
as
do_all_back,
until_do
as
until_do_back,
do_until
as
do_until_back,
do_if
as
do_if_back,
do_for
as
do_ for_back,
search
as
search_back,
forall
as
forall_back,
exists
as
exists_back,
until_continue
as
until_continue_back,
continue_until
as
continue_until_back,
continue_for
as
continue_for_back,
continue_search
as
continue_search_back
redefine
target
end
feature -- Status report
target:
BILINEAR
[G]
-- The structure to which iteration features will
-- apply
feature -- Cursor movement
finish
is
-- Move cursor of target to last position.
do
target.finish
end
back
is
-- Move cursor of target backward one position.
do
target.back
end
end
This class provides a good example of the economy of expression that the full inheritance mechanism affords through the combination of renaming, redefinition, repeated inheritance rules and selection, without sacrificing clarity and maintainability.
Tree iterations, provided by class
CURSOR_TREE_ITERATOR,
work on trees of the cursor tree form; only with this form of tree are traversal operations possible.
Three forms of iteration are provided: preorder, postorder and breadth-first. They
correspond to the three traversal policies described in the discussion of trees.
Here too it would seem that a rather lengthy class is needed, but repeated
inheritance works wonders.
CURSOR_TREE_ITERATOR
simply inherits three times from
LINEAR_ITERATOR,
renaming the features appropriately in each case:
All it needs to do then is to redefine the type of target to be CURSOR_TREE [G], and to redefine six features: the three renamed start (pre_start etc.) and the three forth (pre_ forth and so on). These seven redefinitions give us a full-fledged battery of tree iteration mechanisms.
To conclude this discussion, let us now put together the various mechanisms studied so
far, to see how authors of client software can use the Iteration library to perform
possibly complex iterations on various data structures without ever writing a single loop
or test. The basic ideas were sketched above but now we have all the elements for the
full view.
An application class may use one of the iteration classes in either of two ways: as a
descendant (single or repeated) or as a client. The descendant technique is extremely
simple but less versatile.
Assume an application class
PROCESSOR
that is a proper descendant of one of the effective iteration classes studied in this chapter.
Then a routine of
PROCESSOR, say
iterate, may iterate a certain action over a data structure, subject to a certain test.
First, class PROCESSOR must specify the action by redefining
item_action and
item_test
(or, in the most general case,
action and
test).
Then routine iterate must specify the target data structure through a call of the form
set
(t)
where t represents the selected target data structure. The type of
t must correspond to the iteration class selected as ancestor of
PROCESSOR:
for
LINEAR_ITERATOR
it must be a descendant of
LINEAR
(such as
LINKED_LIST,
ARRAYED_LIST,
LINKED_CIRCULAR
or any other list or circular chain classes); for
TWO_WAY_CHAIN_ITERATOR
it must be a descendant of
BILINEAR
such as
TWO_WAY_LIST
or
TWO_WAY_CIRCULAR;
for
CURSOR_TREE_ITERATOR
it must be a descendant of
CURSOR_TREE.
In all cases the actual generic parameters of the iterator class and of the data structure class must be compatible.
Then the iteration proper is obtained simply by calling the appropriate procedure, without any qualification
or arguments, for example:
do_ if
It is hard to imagine a simpler scheme: no loops, no initialization, no arguments.
Feature
item_action
may need to rely on some variable values. Because it does not take any
argument, such values will have to be treated as attributes, with the corresponding set_...
procedures to set and change their values. This also applies to the two schemes set next.
The single descendant technique has one drawback: it provides the iterating class,
PROCESSOR,
with only one set of iteration particulars. This limitation does not affect
the number of targets: you may use as many targets as you wish, as long as they are of
compatible types, by calling a routine such as iterate several times, or calling several
such routines, each call being preceded by a call to set to define a new target. The
limitation also does not affect the iterating scheme: one iteration could use
do_if,
the next
do_all
and so on. But it does require the action and test to be the same in all cases.
The next two techniques will remove this limitation.
One way to obtain several iteration schemes is a simple extension to the single descendant technique. You can use repeated inheritance to provide two or more variants. We have in fact already encountered the technique when studying how to derive TWO_WAY_CHAIN_ITERATOR and CURSOR_TREE_ITERATOR from LINEAR_ITERATOR. The general pattern, applied here to just two iteration schemes but easily generalized to more, is straightforward:
class
DUAL_PROCESSOR
inherit
LINEAR_ITERATOR
[SOME_TYPE]
rename
item_action
as action1,
item_test
as test1,
do_if
as do_if1,
redefine
action1, test1
select
action1, test1
end
LINEAR_ITERATOR
[SOME_TYPE]
rename
item_action
as action2,
item_test
as test2,
do_if
as do_if2,
redefine
action2, test2
end
feature
action1 is
-- Action for the first scheme
do
...
end
test1:
BOOLEAN is
-- Test for the first scheme
do
...
end
action2 is
-- Action for the second scheme
do
...
end
test2:
BOOLEAN is
-- Test for the second scheme
do
...
end
iterate1 is
-- Execute iteration of first kind.
do
set
(...)
do_if1
end
iterate2 is
-- Execute iteration of second kind.
do
set
(...)
do_if2
end
...
The repeated inheritance machinery takes care of the rest.
To obtain maximum flexibility, classes that need iteration facilities should be clients
rather than descendants of the iteration classes. The resulting scheme is completely
dynamic: to perform iterations you use iterator objects as discussed earlier.
The following example illustrates the technique. Consider a deferred class
FIGURE describing the notion of graphical figure,
with many effective descendants (POLYGON,
CIRCLE and so on). It is useful to define
COMPLEX_FIGURE, describing figures that
are recursively composed of sub-figures. This is a remarkable example of multiple
inheritance:
class
COMPLEX_FIGURE
inherit
FIGURE,
LINKED_LIST [FIGURE]
feature
...
end
In the feature clause we want to provide the appropriate effectings for the deferred
features of class FIGURE:
display, hide,
translate and all other basic figure operations.
We can use loops for that purpose, for example
display is
-- Recursively display all components of the complex
-- figure
do
from
start
until
exhausted
loop
item.display
forth
end
end
Although acceptable and even elegant, this scheme will cause significant
duplication: all the FIGURE features - not
just display but also hide,
rotate, move and
others - will have the same structure, with a loop. We can use iterators to avoid this
duplication. The repeated inheritance technique would work, but given the large number
of FIGURE features the amount of repeated inheritance that would be needed seems
unwieldy. It is also not very desirable to have to change the inheritance structure of the
system just to add a new feature to FIGURE. The more dynamic approach using iterator
objects seems preferable.
To implement this approach, define a class for iterating on complex figures:
class
COMPLEX_FIGURE_ITERATOR
inherit
LINEAR_ITERATOR
redefine
target
end
create
set
feature
target:
COMPLEX_FIGURE
end
Then for each operation to be iterated define a small class. For example:
class
FIGURE_DISPLAYER
inherit
COMPLEX_FIGURE_ITERATOR
redefine
item_action
end
create
set
feature
item_action
(f:
FIGURE) is
-- Action to be applied to each figure: display
-- it.
do
f.display
end
end
Similarly, you may define FIGURE_HIDER, FIGURE_MOVER and others. Then the features of COMPLEX_FIGURE are written almost trivially, without any explicit loops; for example:
display is
-- Recursively display all components of the complex
-- figure
local
disp: FIGURE_DISPLAYER
do
create
disp.set
(Current)
disp.do_all
end
and similarly for all the others.
Note the use of set as creation procedure, which is more convenient than requiring
the clients first to create an iterator object and then to call set. This is also safer, since
with set as a creation procedure the client cannot forget to initialize the target. (If a class
C has a creation clause, the creation instruction
create x
with no creation call is invalid for x of type
C.)