Home Design Patterns Iterator

Iterator

Behavioral Gang of Four

“Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.”

Gamma, Helm, Johnson & Vlissides, p. 257

Purpose

An Iterator separates the job of walking a collection from the collection itself. The client asks for a cursor and then advances it, and at no point does it learn whether it is standing on an array, a linked list, a hash table or a tree.

Two things fall out of that. The collection is free to change its internal representation without breaking any caller, which is the encapsulation argument. And an algorithm written against the cursor works on every collection that can produce one, which is the reuse argument. The second turned out to be the more valuable of the two, and it is the one the C++ standard library was built on.

Design

The Aggregate declares an operation that returns an Iterator, which makes it a Factory Method. The Iterator declares the cursor operations: start, advance, test for the end, read the current element. Each concrete collection supplies a matching concrete iterator that knows the private layout of the collection it came from, which is why the iterator is usually written as a nested or friend class.

UML class diagram: Aggregate creates an Iterator; ConcreteAggregate creates a ConcreteIterator which holds a reference back to the aggregate.
Note the two dashed lines from the Client. It depends on both interfaces and neither implementation, which is the entire point: the loop it writes does not change when the collection does.

The main variation is who drives. An external iterator hands the cursor to the client, which decides when to advance and can stop early or run two cursors at once. An internal iterator keeps control and calls a function you supply for each element, which is simpler to write and impossible to break out of cleanly. Most languages give you external iteration in the loop syntax and internal iteration in the library, as foreach versus List.ForEach.

Example

The interesting half of this at Autobot Robotics was on the embedded controller, where there is no foreach, no allocation after startup and no standard library worth the name. The controller keeps three collections with three completely different shapes: a fixed-size ring buffer of recent vision frames, a free-list of motion segments linked through their own storage, and a static array of axis records.

The diagnostics and telemetry code needed to walk all three. The first version had three copies of every reporting routine, one per container, and they drifted. The ring buffer copy had a wrap-around bug that the array copy could not have had, and it was found in the field because a fault report printed the last eight frames in the wrong order.

The fix was a cursor struct with moveNext() and current(), one implementation per container, allocated on the stack. The reporting routines were written once. Meanwhile the office-side reporting service, which is C# and reads the same data out of PostgreSQL, never wrote an iterator at all: it wrote yield return and let the compiler generate one. Same pattern, and only one of the two teams had to know it existed.

interface FrameIterator {
    bool moveNext();
    Frame current();
}

class FrameRing {                        // the aggregate
    private Frame[] slots;
    private int head, count;

    public FrameIterator getIterator() { return new RingIterator(this); }
}

class RingIterator : FrameIterator {     // the concrete iterator
    private FrameRing ring;
    private int seen;

    public RingIterator(FrameRing r) { ring = r; seen = 0; }

    public bool moveNext() {
        // step the cursor, handle the wrap, return false at the end
    }

    public Frame current() {
        // return the frame the cursor is standing on
    }
}

class Diagnostics {
    public void dump(FrameIterator it) {
        while (it.moveNext()) {
            // report on it.current()
        }
    }
}

// The same thing on the office side. The compiler writes the iterator.
class FrameLog {
    public IEnumerable<Frame> recent() {
        for (int i = 0; i < count; i++) {
            yield return slots[(head + i) % slots.Length];
        }
    }
}
dump() takes the interface, not the ring, and that is the pattern. The last six lines are the same pattern after the language ate it: one yield return replaces the whole RingIterator class.

Seen in the Wild

This is the pattern most thoroughly absorbed into languages. .NET has IEnumerable and IEnumerator wired into the foreach statement, with yield return generating the iterator class for you. Java has Iterable and Iterator behind its enhanced for loop, and Python has __iter__ and __next__ behind its for statement, plus generators. The C++ standard library goes furthest: its algorithms are written against iterator categories rather than containers, so std::sort works on anything that can produce a random access iterator, including a raw pointer into an array.

Comparisons

  • Factory Method is how the aggregate produces the right iterator without the client naming its class.
  • Composite is the case that makes iterators awkward. Walking a tree with an external iterator means the iterator has to carry its own explicit stack, which is exactly the code the pattern was supposed to hide.
  • Visitor is the usual alternative for trees. An iterator visits nodes in an order it chose; a Visitor lets each node decide how its children are reached.
  • Memento can capture the state of an iteration, so a position can be saved and returned to without exposing what a position is.
  • Pipes and Filters is where lazy iterators end up. A chain of map and filter over an enumerable is a pipeline whose stages happen to be iterators.

Why It Exists

My take

Iterator exists because algorithms outnumber collections and both keep multiplying. Without a common cursor, "sum these", "find the first matching", "write these to a file" have to be written once per container, so M algorithms and N containers cost M times N functions. With a cursor it is M plus N. That is the same arithmetic argument that Bridge makes, and it is the real reason the pattern matters, more than the encapsulation story usually told about it.

It is also the pattern I would tell someone not to implement. In C#, Java or Python you will never write one by hand: you write yield return, or you implement two methods that the language already knows about. What you should take from the chapter is not how to build an iterator but what the language is doing on your behalf, because the consequences leak. A lazy enumerable is a description of work, not the work, so it can be enumerated twice and do the query twice, or be enumerated after the connection it depends on has closed. Those bugs are the pattern's semantics showing through the syntax.

Where you do still write them by hand is where I spent most of my career. On an embedded controller with no heap and no standard library, an iterator is a small struct on the stack and you write it deliberately, because the alternative is duplicated traversal code and duplicated traversal code is where off-by-one bugs live. The wrap-around bug in the example above is a real class of failure, and having exactly one piece of code that knows how the ring wraps is worth more than any argument about abstraction.

Criticisms

Iterators and mutation get along badly. Modify a collection while a cursor is open and you get, depending on the language, an exception, a skipped element, or undefined behavior reading freed memory. Java's fail-fast iterators throw ConcurrentModificationException as a courtesy; C++ simply invalidates the iterator and lets you find out later. This is not an implementation defect, it is inherent: a cursor is a pointer into a structure whose shape the collection is free to change.

The abstraction also hides cost, which is its job and its problem. Walking a linked list by index is quadratic and looks identical to walking an array. A lazy iterator over a database query can issue a round trip per element, which is the N+1 problem wearing a foreach as a disguise. The code reads as one loop and behaves as a thousand queries.

Finally, the interface is thin to the point of being lossy. A cursor gives you forward, one element at a time, one pass. Anything else, counting without consuming, going backwards, splitting the work across threads, knowing whether the source is finite, needs a richer contract that the plain pattern does not provide. Every mature library ends up growing one, which is what .NET's IQueryable and Java's Spliterator are: admissions that a bare iterator was not quite enough.