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?
answer
- intent survives, mechanism moves into the compiler
- yield = hand-written state machine, generated
- generators are usually single-pass
- keep a class for reset / bidirectional / resumable cursor
- same story: Visitor → pattern matching
basics
~20 sThe 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 sThe 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// 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
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.
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.
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.
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).