Contrast an eager collection pipeline with a lazy Sequence pipeline in Kotlin. When does asSequence() actually pay off, and when does it not?
answer
- Eager = horizontal per-step lists; lazy = vertical per-element
- Nothing runs until a terminal op pulls
- Intermediate (lazy) vs terminal (eager) operators
- Wins: big data, long chains, short-circuit, infinite
- Loses: small/single op (per-element overhead); sorted buffers
basics
~20 sA normal list pipeline processes the whole collection at each step, creating a new list every time. A Sequence processes one element through all steps before moving on, with no intermediate lists, and can stop early. Sequences help on big data or early exit.
solid answer
~40 sEager collection operators (on `Iterable`) evaluate **horizontally**: each step runs over the entire collection and allocates a fresh `List` before the next step. `Sequence` operators are **lazy** and evaluate **vertically**: `asSequence()` builds a chain of lazy stages, nothing runs until a *terminal* operation (`toList`, `first`, `sum`, `count`, `forEach`) pulls elements one at a time through the whole chain. Benefits: no intermediate allocations, and short-circuiting terminals (`first`, `any`, `take`) stop processing early. Sequences shine for large inputs, many chained steps, expensive transforms, infinite sources (`generateSequence`), and early-exit queries. They do NOT pay off for small collections or single operations: the per-element iterator/closure overhead can make them *slower*, and you lose the simple eager mental model. Operators split into **intermediate** (lazy: map, filter) and **terminal** (eager: toList, fold).
code
kotlin · 8 lines// Short-circuit: find first even square > 10 without processing the rest
val answer = generateSequence(1) { it + 1 } // infinite 1,2,3,...
.map { it * it }
.filter { it % 2 == 0 }
.first { it > 10 } // terminal, stops early
println(answer) // 16
// Eager equivalent over an infinite source would never terminate.go deeper
Knows sequences are 'lazy' and lists are 'eager' at a high level.
Explains intermediate vs terminal ops and that nothing runs until a terminal call.
Reasons about horizontal vs vertical evaluation, short-circuiting, stateful ops, and when sequences are actually slower.
Sets guidance on when to default to sequences vs collections, considers GC/allocation profiles, one-shot constraints, and measures rather than assumes.
## Two evaluation orders Given `filter` then `map` over `[1,2,3,4]`: - **Eager (collection)** — *horizontal*: `filter` runs over all four → new list `[2,4]`; then `map` runs over all of that → new list `[4,16]`. Each step materializes a full intermediate `List`. - **Lazy (sequence)** — *vertical*: element `1` flows filter→map, then `2`, then `3`… No element is fully transformed until requested, and no intermediate list exists. ```kotlin val r = listOf(1, 2, 3, 4, 5) .asSequence() .filter { println("filter $it"); it % 2 == 0 } .map { println("map $it"); it * it } .first() // terminal: pulls just enough // Prints: filter 1, filter 2, map 2 -> stops at first match (4) ``` With the eager version the same chain prints all `filter` lines, then all `map` lines, and never short-circuits. ## Intermediate vs terminal operators - **Intermediate** (return a `Sequence`, lazy, do nothing yet): `map`, `filter`, `flatMap`, `take`, `drop`, `distinct`, `sorted`*. - **Terminal** (trigger evaluation, return a value/collection): `toList`, `toSet`, `first`, `count`, `sum`, `fold`, `forEach`, `any`. *`sorted` is *stateful*: it must buffer all elements, so it breaks pure streaming even within a sequence. ## Building sequences - `asSequence()` wraps an existing `Iterable`. - `sequenceOf(…)`. - `generateSequence(seed) { next }` for infinite/lazy generation (combine with `take`). - `sequence { yield(…); yieldAll(…) }` — a coroutine-based builder. ## When it pays off - Large collections where intermediate lists are costly (memory + GC). - Long chains of many operators. - Short-circuiting terminals (`first`, `find`, `any`, `take(n)`). - Infinite or expensive-to-produce sources. ## When it does NOT pay off - Small collections (a few hundred items): iterator + lambda overhead per element often makes sequences *slower* than a couple of eager passes. - A single operation (`list.map { }`): no intermediates to save. - Needs that require a full buffer anyway (`sorted`, `groupBy`) negate streaming gains. ## Re-iteration caveat A `Sequence` from `generateSequence`/`sequence { }` may be **constrained** (one-shot) — iterating twice throws `IllegalStateException`. `asSequence()` over a list is re-iterable. Don't assume a sequence is reusable.
- Why can a Sequence pipeline be slower than the equivalent list pipeline?Sequences add per-element iterator and lambda-invocation overhead at every stage. For small inputs that overhead exceeds the cost of allocating one or two intermediate lists, so the eager version wins.
- Does putting sorted() in a sequence keep it fully lazy?No. sorted is a stateful intermediate operation that must buffer and consume all elements before emitting any, so it breaks element-at-a-time streaming and prevents short-circuiting downstream.
Eager is an assembly line that finishes every car at station 1 before any moves to station 2; lazy is one car walked through all stations end-to-end, so you can ship the first finished car immediately.
saying these in an interview costs you the question
- Claiming asSequence() is always faster
- Thinking a sequence runs immediately when defined (no terminal)
- Not knowing intermediate vs terminal operator distinction
- Assuming all sequences are re-iterable
- Believing sorted/groupBy stream lazily inside a sequence