Sequences are lazily/deferred-evaluated. What surprising behaviors or pitfalls does this deferral cause around side effects, single-iteration, and exception timing?
answer
- Work deferred to the terminal op, on its thread
- Two terminals re-run the whole pipeline (side effects double)
- One-shot/constrainOnce sources throw on second iteration
- Exceptions surface at the terminal call site
- Keep lambdas pure; materialize to snapshot
basics
~20 sBecause work is deferred until a terminal op, side effects in map/filter don't run when you write them, exceptions surface later, and some sequences can only be iterated once or re-run their work on each terminal call.
solid answer
~50 sDeferral means intermediate operators capture lambdas but don't execute; the work happens at the terminal op, on the thread that calls it. Pitfalls: (1) **Side-effect timing** — logging/IO inside map runs at terminal time, not at definition time, which surprises people relying on ordering. (2) **Re-evaluation** — a `Sequence` is a recipe; calling two terminals on the same sequence value (e.g. `count()` then `toList()`) re-runs the whole pipeline twice, re-firing side effects. (3) **Single-use sources** — sequences built from a one-shot iterator (like a Stream or `Iterator.asSequence()`) throw `IllegalStateException: This sequence can be consumed only once`. (4) **Exception timing** — an exception in a lambda is thrown from the terminal call, not where the operator was written, complicating stack traces and try/catch placement. (5) **Capturing mutable state** in lambdas is dangerous because of deferred, possibly repeated execution.
code
kotlin · 11 linesval seq = (1..3).asSequence().onEach { println("touch $it") }
println("-- count --")
seq.count() // touch 1, touch 2, touch 3
println("-- toList --")
seq.toList() // touch 1, touch 2, touch 3 (re-evaluated!)
// Snapshot to consume multiple times safely:
val materialized = seq.toList()
materialized.size; materialized.first() // no re-evaluationgo deeper
Knows work happens at the terminal, not when map is written.
Identifies re-evaluation on multiple terminals and that side effects can double.
Covers one-shot/constrainOnce sources, exception timing at the terminal, and the purity requirement for lambdas.
Sets team conventions (pure pipelines, materialize-to-snapshot, terminal-scoped error handling) and reasons about correctness risks when refactoring eager↔lazy.
## Deferred execution: the root cause Intermediate sequence operators (`map`, `filter`, …) only **store** their lambda inside a wrapper. Real execution is **deferred** to a **terminal operation** (`toList`, `count`, `forEach`, `first`). Everything below follows from that. ## Pitfall 1 — side effects fire late ```kotlin val s = listOf(1, 2, 3).asSequence().map { log("mapping $it"); it * 2 } println("built") // prints first — nothing logged yet val r = s.toList() // NOW the 'mapping' logs fire ``` If you assumed the mapping logged during construction, you are wrong. ## Pitfall 2 — terminals re-run the whole pipeline A `Sequence` value is a *plan*, not a cached result. Each terminal re-pulls from the source: ```kotlin val s = (1..3).asSequence().onEach { println("touch $it") } s.count() // touch 1, touch 2, touch 3 s.toList() // touch 1, touch 2, touch 3 -- AGAIN ``` Side effects double; expensive upstream work repeats. Materialize once (`toList()`) if you need to consume multiple times. ## Pitfall 3 — single-iteration / one-shot sources Sequences backed by a consumable iterator can only be iterated once. The constrained constructor (`Iterable.asSequence()` on a one-shot iterable, or `Iterator<T>.asSequence()`) yields a sequence that throws on a second pass: ```kotlin val once = sequenceOf(1, 2, 3).constrainOnce() once.toList() // ok once.toList() // IllegalStateException: This sequence can be consumed only once. ``` ## Pitfall 4 — exception timing & stack traces An exception thrown inside a `map` lambda surfaces from the **terminal** call site, not where `map` was written. `try/catch` must wrap the terminal operation, and stack traces point at the consumer, which can mislead debugging. ```kotlin val s = listOf(0).asSequence().map { 1 / it } // no throw here try { s.toList() } catch (e: ArithmeticException) { /* caught HERE */ } ``` ## Pitfall 5 — capturing mutable state Because execution is deferred and may repeat across terminals, lambdas that mutate or read external mutable state can observe stale/changed values or accumulate incorrectly. Keep sequence lambdas pure. ## Takeaways - Treat a `Sequence` as lazy and re-runnable; materialize with `toList()` when you need a stable, multiply-consumed snapshot. - Put `try/catch` around the terminal. - Avoid side effects in intermediate operators; prefer `onEach` only when you understand it fires per terminal.
- How do you safely consume a sequence's results multiple times without re-running upstream work?Materialize once with toList()/toSet() and reuse the resulting collection; collections cache their elements.
- Where should you place try/catch for an exception thrown inside a sequence's map lambda?Around the terminal operation (e.g. toList/forEach), because that's where deferred evaluation actually runs the lambda.
- What does constrainOnce() do?Wraps a sequence so it can be iterated only once; a second terminal throws IllegalStateException.
saying these in an interview costs you the question
- Assuming a Sequence caches results so multiple terminals are free
- Wrapping try/catch around the map call instead of the terminal
- Believing every Sequence can be iterated unlimited times
- Putting important side effects in intermediate operators
- Thinking side effects fire when the operator is written, not at the terminal