skip to content

The Iterator pattern is a Gang of Four design pattern, yet many languages provide generators or comprehensions built in. When a language feature subsumes a pattern, what changes and what does not?

level: middleimportance: should knowfreq 30%

answer

  1. intent survives, mechanism moves into the compiler
  2. yield = hand-written state machine, generated
  3. generators are usually single-pass
  4. keep a class for reset / bidirectional / resumable cursor
  5. same story: Visitor → pattern matching

basics

~20 s

The problem stays the same — traverse a collection without exposing its internals. What changes is the cost: instead of hand-writing an iterator class with hasNext/next, you write a generator or comprehension. Writing the class version anyway is unidiomatic and adds state and bugs.

solid answer

~50 s

The pattern's intent — traverse an aggregate sequentially without exposing its representation, and allow multiple independent traversals — is unchanged. What the language feature changes is who writes the mechanism. A generator (a function that suspends at yield and resumes on demand) lets the compiler or runtime build the state machine that you would otherwise hand-code as fields in an iterator class; comprehensions and lazy stream/sequence APIs cover the common map/filter/take pipelines outright. You gain far less code, no manual state, and laziness by default; you lose some control — explicit iterator objects can support reset, bidirectional or random access, snapshot semantics, resumable checkpoints, or a stable identity you can hand around. You also inherit the language's semantics for fail-fast concurrent modification, exception propagation across suspension points, and whether the sequence is single-pass. So: use the built-in feature by default; reach for an explicit iterator type only when you need capabilities the feature does not express.

code

pseudocode · 8 lines
pseudocode
// Hand-rolled Iterator: object holds the traversal state
class RangeIterator { i = start; hasNext() { return i < end } next() { return i++ } }

// Generator: the runtime builds that state machine for you
function range(start, end) { for (i = start; i < end; i++) yield i }

// Comprehension / lazy pipeline: no traversal code at all
evens = [x for x in range(0, 100) if x % 2 == 0]

go deeper

for a junior

Say the goal — walk a collection without exposing how it stores things — stays the same, and that generators or comprehensions do it with far less code.

for a middle

Explain that a generator is the compiler writing the state machine you would hand-code, mention laziness and single-pass semantics, and give one case where an explicit iterator type is still right.

for a senior

Discuss the consequences the language feature brings (laziness, exception timing, cleanup on abandonment, invalidation) and the capabilities only an explicit cursor gives — reset, bidirectional, resumable, resource-owning.

for a principal

Generalise: patterns are documentation of recurring problems, and recurring solutions migrate into languages over time (Visitor → pattern matching, Strategy → lambdas). Guidance should be about intent, not about typing out 1994 class diagrams.

## What the Iterator pattern actually specifies GoF Iterator: *provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.* Roles: the **Aggregate** (collection) and the **Iterator** (cursor). Key consequences in the original text: the traversal state lives in the iterator, so **multiple simultaneous traversals** are possible; the client is decoupled from the collection's internal structure (array, tree, hash table, network page); and different traversal orders can be offered by different iterator implementations. Note what the pattern does **not** say: nothing about `hasNext`/`next` method names, nothing about classes. Those are the 1994 implementation in the languages of the day. ## What languages absorbed - **Built-in iteration protocol.** `for-each` loops over anything implementing `Iterable`/`IEnumerable`/`__iter__`/`Symbol.iterator`. The pattern is now part of the *language contract*, which is stronger than a convention: every collection in the ecosystem is traversable the same way. - **Generators / `yield`.** A function that suspends at each `yield` and resumes where it left off. The compiler transforms it into a state machine — exactly the fields you would otherwise maintain by hand in an iterator class. Lazily produces items, supports infinite sequences, and reads like straight-line code. - **Comprehensions / lazy pipelines.** `[f(x) for x in xs if p(x)]`, LINQ, Java Streams, Kotlin Sequences, Rust iterator adaptors. These cover the overwhelmingly common map/filter/take/fold shapes without any traversal code at all. - **Coroutines / async iteration.** `async` generators and `for await` extend the same protocol over I/O. ## What changes when a feature subsumes a pattern 1. **Cost collapses.** Twenty lines of cursor class become one `yield` inside a normal function. 2. **The default flips.** Hand-rolling the class becomes the exception that needs justification, and doing it by default is a review smell ("correct pattern, wrong idiom"). 3. **Uniformity across the ecosystem.** Because the protocol is in the language, third-party collections interoperate with library algorithms for free. 4. **New semantics arrive with the feature.** Laziness (nothing computes until consumed), single-pass sequences that throw if traversed twice, fail-fast behaviour on concurrent modification, exception propagation across suspension points, and cleanup on early abandonment (Python's `GeneratorExit`, `try/finally` inside a generator). These are real behaviours to know, not free lunch. ## What does not change - **The intent and the vocabulary.** You still say "expose an iterator over this" in design discussion. - **The forces.** Hiding representation, allowing independent concurrent traversals, and supporting alternative orders are still the reasons you offer iteration rather than handing out the internal list. - **The consequences you must reason about.** Iterator invalidation when the collection mutates mid-traversal; whether the traversal is a live view or a snapshot; who owns cleanup. ## When to keep an explicit iterator type - **Extra capabilities:** bidirectional or random access, `reset`, peek/lookahead, index reporting, `remove` during traversal. - **Restartable or multi-pass traversal:** a generator instance is usually consumed once; a class can be re-created or reset, and an `Iterable` factory can hand out fresh cursors. - **Resumable/checkpointable cursors:** paginated API or database cursors whose position must be serialised and resumed later — the position must be data. - **Explicit resource ownership:** a cursor holding a connection or file handle, where callers must close it deterministically. - **Snapshot vs live-view semantics** you must state and control explicitly. ## Generalising beyond Iterator The same absorption happened elsewhere: **Visitor → pattern matching over sum types with exhaustiveness checks**; **Strategy/Command → first-class functions**; **Singleton → module objects or DI-container scopes**; **Null Object → option types**; **Observer → built-in events/reactive streams/channels**; **Decorator over one operation → function composition**. The generalisable claim: *patterns document recurring problems; when a problem recurs enough, language designers push the solution into the language, and the pattern survives as vocabulary rather than as code you type.*

  • Name a case where you would still hand-write an iterator type instead of using a generator.
    A paginated database or API cursor whose position must be serialised and resumed after a restart, or one that owns a connection needing deterministic close — the position and the resource must be inspectable data, and you may also need reset, peek or bidirectional movement.
  • What behavioural surprises come with lazy generator-based iteration?
    Nothing executes until consumption, so exceptions and side effects surface at a different place than where the pipeline was defined; many sequences are single-pass and fail or silently yield nothing on a second traversal; abandoning a generator early triggers its cleanup path; and mutating the source mid-traversal may throw fail-fast errors.
  • Which other GoF patterns were absorbed into language features in the same way?
    Visitor by pattern matching over sum types with compiler exhaustiveness checks, Strategy and Command by first-class functions and closures, Singleton by module-level objects or DI lifetime scopes, Null Object by option/maybe types, and Observer by built-in events, reactive streams or channels.

saying these in an interview costs you the question

  • Claiming the Iterator pattern 'no longer exists' rather than saying it moved into the language contract.
  • Hand-writing hasNext/next classes in a language with generators, then calling it 'proper design'.
  • Assuming a generator can be traversed twice like a collection.
  • Ignoring iterator invalidation and fail-fast semantics when the source mutates during traversal.
  • Believing comprehensions are only syntactic sugar with no semantic consequences (laziness, single-pass, cleanup on abandonment).

context