Iterator Pattern with Sequences

Learn how Clojure sequences, lazy evaluation, and transducers cover most Iterator needs, and when a custom sequence or reducible shape is a better fit than emulating object-style iterators.

Iterator pattern: A way to traverse a collection or data source without exposing its full internal representation to the caller.

In Clojure, most Iterator-style needs are already covered by sequences. That changes the role of the pattern. Instead of building explicit iterator objects with next and hasNext, Clojure usually asks a simpler question: how do you expose a sequenceable or reducible view over this data?

Sequences Already Solve Most Iteration Problems

If your data can be represented as a sequence, consumers immediately gain access to the standard collection toolkit:

  • map
  • filter
  • reduce
  • take
  • drop
  • into

That is often better than a custom object-style iterator because the traversal protocol is already part of the language’s normal vocabulary.

A Custom Sequence View Is Often Enough

1(defrecord LogCursor [entries]
2  clojure.lang.Seqable
3  (seq [_]
4    (seq entries)))

This is often a more Clojure-native answer than creating an imperative cursor API. The caller gets ordinary sequence semantics and can use all the familiar collection operations.

Laziness Changes the Pattern

One reason Iterator feels different in Clojure is lazy evaluation. The traversal can be incremental without requiring an explicit iterator object:

1(defn numbered-lines [lines]
2  (map-indexed (fn [idx line]
3                 {:line-number (inc idx)
4                  :text line})
5               lines))

Nothing forces the entire traversal to happen at once. That means many “build an iterator” use cases become “return a lazy sequence” instead.

When Sequences Are Not Enough

Sequences are not always the right answer. You may want a reducible process or transducer-friendly shape when:

  • the data source is large
  • intermediate sequence allocation is too expensive
  • you want one-pass streaming reduction
  • backpressure or I/O semantics matter

In those cases, the right abstraction may be a reducible or stream-processing API, not a classic iterator.

A Useful Mental Model

The visual below shows how Clojure often replaces an explicit iterator object with a sequenceable view over a data source.

    flowchart LR
	    A["Data source"] --> B["Seq or reducible view"]
	    B --> C["map/filter/take/reduce"]
	    C --> D["Consumer"]

The key shift is that iteration behavior is surfaced through a standard collection interface rather than a custom cursor protocol.

Common Failure Modes

Iterator-style designs go wrong in Clojure when:

  • object-style cursor APIs are recreated unnecessarily
  • laziness is introduced where deterministic eager evaluation is needed
  • sequence realization costs are ignored
  • the underlying resource lifecycle is not made explicit

If the sequence depends on a file handle, socket, or transaction, the resource boundary still needs careful design.

Practical Heuristics

Start with sequences. Move to transducers or reducible processes when performance or streaming needs justify it. Only emulate classic iterators when you truly need that specific interface for interop or boundary compatibility.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026