Can you call two terminal operations on the same Sequence instance? What happens, and how does this differ from calling a terminal op without one at all?
answer
- No terminal op = no execution
- Re-iterable sources re-run the chain each terminal
- Iterator.asSequence() is single-use -> throws on 2nd terminal
- No memoization between terminal calls
- Materialize with toList() to reuse a result
basics
~20 sSome sequences can only be iterated once, so running a second terminal op on them throws. A sequence built from a collection can be re-run. And without any terminal op, the chain never executes at all.
solid answer
~40 sWhether you can run a terminal op twice depends on how the sequence was created. Sequences backed by a re-iterable source (e.g. `list.asSequence()`, `sequenceOf(...)`) can be consumed multiple times — each terminal op re-runs the whole intermediate chain from the start. But **constrained/single-use** sequences — notably the one returned by `Iterator.asSequence()` or some `sequence { }` builders over a one-shot source — throw `IllegalStateException("This sequence can be consumed only once")` on the second terminal op, because the underlying iterator is already exhausted. Separately, with **no** terminal op the intermediate chain is never executed at all — a common bug. Key takeaways: a terminal op is required to run anything; intermediate ops are re-evaluated on each terminal call (no caching); and don't assume a sequence is replayable unless its source is.
code
kotlin · 7 linesval once = listOf(1, 2, 3).iterator().asSequence()
println(once.toList()) // [1, 2, 3]
println(once.toList()) // throws IllegalStateException: consumed only once
val many = listOf(1, 2, 3).asSequence()
println(many.toList()) // [1, 2, 3]
println(many.toList()) // [1, 2, 3] again — finego deeper
Understands that a terminal op is needed and that calling toList twice usually works on a list-backed sequence.
Distinguishes re-iterable vs single-use sources and knows the second terminal can throw IllegalStateException.
Explains the iterator-factory model, lack of memoization, and the cost of multiple terminals on expensive chains.
Designs sequence-returning APIs with explicit consumption semantics (materialize vs lazy) and documents single-use constraints to prevent misuse.
## Re-running terminals, single-use sequences, and the missing-terminal trap ### A terminal op is mandatory to execute anything Intermediate ops only build a lazy description. If you never call a terminal op, **zero** lambdas run: ```kotlin val s = sequenceOf(1, 2, 3).map { println(it); it } // prints NOTHING ``` Adding `s.toList()` is what triggers execution. Forgetting the terminal is a classic silent bug. ### Re-iterable vs constrained (single-use) sequences Sequences differ by **source**: - **Re-iterable**: `sequenceOf(...)`, `listOf(...).asSequence()`, `generateSequence { ... }`. Each terminal op restarts iteration from the beginning, re-running every intermediate lambda. There is **no memoization** — running `count()` then `toList()` evaluates the chain twice. - **Constrained / single-use**: created from a one-shot `Iterator`, e.g. `myIterator.asSequence()`, or a `sequence { }` block reading a one-shot source. The standard library wraps these so a second iteration throws: ```kotlin val once = listOf(1, 2, 3).iterator().asSequence() once.toList() // [1, 2, 3] once.toList() // IllegalStateException: This sequence can be consumed only once ``` ### Why the difference exists A `Sequence` is essentially a factory for an `Iterator`. A re-iterable source can hand out a fresh iterator each time; a single-use source has only one underlying iterator, which gets exhausted. The library guards re-use with `constrainOnce()`-style logic to fail fast instead of silently yielding empty results. ### No caching between terminals Because intermediate ops are re-evaluated per terminal call, expensive pipelines run multiple times if you call several terminals. If you need the materialized result more than once, call a terminal once (e.g. `toList()`) and reuse that list. ```kotlin val seq = (1..1_000_000).asSequence().map { expensive(it) } val a = seq.sum() // runs expensive() a million times val b = seq.count() // runs expensive() AGAIN a million times // Better: val materialized = seq.toList(); then sum()/count() on the list ``` ### Mental checklist - No terminal -> nothing executes. - Terminal re-runs the whole intermediate chain (no cache). - Single-use sequences throw on the 2nd terminal; re-iterable ones don't.
- If you call sum() then count() on the same generateSequence pipeline with an expensive map, how many times does map run?Twice over the full sequence — once per terminal — because there is no caching; materialize with toList() first to avoid the recompute.
- Which factory gives a single-use sequence?Iterator.asSequence() (and constrainOnce()); calling a terminal twice on it throws IllegalStateException.
saying these in an interview costs you the question
- Assuming every sequence can be terminal-consumed repeatedly
- Thinking intermediate results are cached between terminal calls
- Not knowing Iterator.asSequence() is single-use
- Believing a second terminal silently returns empty rather than throwing
- Forgetting that no terminal means no execution