skip to content

A Sequence built with sequence{} is iterated twice. What happens, and what does that imply about side effects in the builder block?

level: middleimportance: should knowfreq 28%

answer

  1. Re-iterable but NOT memoized — re-runs block each time
  2. Side effects repeat on every consumption
  3. Local state resets per iteration (good); external state mutates repeatedly (bad)
  4. toList() to consume once and reuse
  5. constrainOnce() throws on second iteration

basics

~10 s

Each iteration re-runs the builder block from the top. So the values are recomputed, and any side effects (like printing) happen again every time you consume the sequence.

solid answer

~50 s

The Sequence returned by `sequence {}` is re-iterable but **not memoized**: every terminal operation creates a fresh iterator that runs the builder block again from the beginning. So if the block has side effects (logging, incrementing an external counter, reading a file), they re-execute on each consumption. The block also typically holds its own local state (loop variables) that is reset per iteration, which is correct. Two pitfalls: (1) capturing and mutating *external* mutable state from the block makes repeated iteration give different results or corrupt that state; (2) consuming an expensive sequence multiple times silently repeats the work. If you need the results once, call `toList()` and reuse the list, or wrap with a memoizing structure. This is the same contract as Java streams' opposite: streams are single-use, whereas a Kotlin Sequence is multi-use but recomputes each time.

go deeper

for a junior

Understands the values are recomputed when you iterate again.

for a middle

Explains side-effect repetition, local vs external state, and toList()/constrainOnce().

for a senior

Designs builder blocks to be pure and chooses materialization vs constrainOnce deliberately.

for a principal

Reasons about idempotency, replayability, and cost across a pipeline, including contrasts with Stream/Flow semantics.

## Re-iterable, not cached A `Sequence<T>` from `sequence {}` can be iterated as many times as you like, but it does **not** cache results. Each terminal operation (`toList()`, `forEach`, `count()`, `for`-loop, …) obtains a **new iterator**, which restarts the builder lambda from the top. ```kotlin var calls = 0 val s = sequence { calls++ // side effect println("producing") yield(1); yield(2) } s.toList() // prints "producing", calls = 1 s.toList() // prints "producing" AGAIN, calls = 2 ``` ## Implications for side effects - **Local state is fine.** Loop counters and accumulators declared *inside* the block reset each run, which is exactly what you want. - **External mutable state is dangerous.** If the block mutates a variable captured from the enclosing scope, each iteration mutates it again — non-idempotent and surprising. - **Expensive work repeats.** Reading a file, hitting a DB, or heavy computation inside the block happens on *every* consumption. ```kotlin val ids = mutableListOf<Int>() val s = sequence { for (i in 1..3) { ids.add(i); yield(i) } } s.toList(); s.toList() println(ids) // [1, 2, 3, 1, 2, 3] — duplicated! ``` ## How to consume once - Materialize: `val result = s.toList()` then reuse `result`. - Or use `.constrainOnce()` to make a sequence that **throws** if iterated twice — useful to catch accidental double consumption of a single-shot source (e.g. wrapping an Iterator). ```kotlin val once = s.constrainOnce() once.toList() // ok once.toList() // throws IllegalStateException ``` ## Contrast with Java Stream and Flow - **Java `Stream`**: single-use; consuming twice throws. Kotlin `Sequence` is multi-use but recomputes. - **`Flow`**: cold like a sequence — each collection re-runs the producer — unless you make it hot (e.g. `SharedFlow`/`StateFlow`) or cache. ## Rule of thumb Keep `sequence {}` blocks **pure** (or at least idempotent). If a value source can only be read once, wrap with `constrainOnce()`; if you need the result repeatedly, compute it once into a `List`.

  • How do you guarantee a sequence is consumed only once?
    Wrap it with .constrainOnce(); a second iteration throws IllegalStateException — useful when the underlying source (e.g. an Iterator) can't be replayed.
  • How do you avoid recomputing an expensive sequence?
    Materialize it once with toList()/toSet() and reuse the resulting collection, rather than re-consuming the sequence.

saying these in an interview costs you the question

  • Assuming a Sequence caches/memoizes its elements
  • Putting non-idempotent side effects in the builder and reusing the sequence
  • Thinking a Kotlin Sequence is single-use like a Java Stream
  • Mutating captured external state from inside the block

context