When should you use a Sequence instead of eager collection operators in commonMain, and what are the cross-platform performance and laziness implications?
answer
- Collections eager/horizontal; sequences lazy/vertical
- Intermediate ops lazy, terminal ops trigger
- Sequences: no intermediate lists + short-circuit + infinite
- Small data -> eager is simpler/faster
- kotlin.sequences is common; single-pass
basics
~20 sCollection operators like map/filter build a new list at each step. A Sequence is lazy: it processes one element through all steps before moving on, so it avoids intermediate lists and can stop early. Use sequences for long pipelines or large/infinite data.
solid answer
~40 sEager collection operators (`map`, `filter`, `take`) each allocate a **new intermediate List** and process the whole collection per step — horizontal, breadth-first. A `Sequence` (from `asSequence()` or `generateSequence()`/`sequence { yield(...) }`) is **lazy and vertical**: each element flows through the entire chain of *intermediate* operations before the next element starts, and nothing runs until a *terminal* operation (`toList`, `first`, `sum`, `count`) pulls. This avoids intermediate allocations, enables **short-circuiting** (e.g. `first { }` stops early), and supports **infinite** sequences. The trade-off: sequences add per-element iterator overhead, so for small collections eager is often faster and simpler. All of `kotlin.sequences` is **common**, so the same reasoning holds on JVM, JS, and Native (note JS lacks JVM JIT, making intermediate-allocation savings more pronounced there). Sequences are single-pass and stateless per terminal call.
code
kotlin · 7 lines// Find first user whose expensive score passes — short-circuits
fun firstQualified(users: List<User>): User? =
users.asSequence()
.filter { it.active }
.map { it to expensiveScore(it) } // only runs until a match
.firstOrNull { (_, score) -> score > 0.9 }
?.firstgo deeper
Knows map/filter create new lists and that sequences are 'lazy'.
Distinguishes intermediate vs terminal ops and uses asSequence for short-circuiting.
Reasons about allocation, vertical evaluation, infinite sequences, and when eager is actually better.
Sets pipeline conventions and benchmarks the trade-off per platform (JS/Native allocation pressure vs JVM JIT).
## Eager (collections) vs lazy (sequences) ```kotlin listOf(1,2,3,4).map { it*it }.filter { it > 4 } // map -> [1,4,9,16] (new list) // filter -> [9,16] (another new list) ``` Each operator is **eager**: it computes a full new `List` and only then hands it to the next operator. This is *breadth-first / horizontal* evaluation: every element completes `map` before any starts `filter`. A **Sequence** flips this to *depth-first / vertical*, lazy evaluation: ```kotlin listOf(1,2,3,4).asSequence() .map { it*it } // intermediate: nothing runs yet .filter { it > 4 } // intermediate .first() // terminal: pulls one element through the whole chain ``` With a sequence, element `1` goes map→filter, then `2`, etc. Nothing executes until a **terminal** operation (`toList`, `first`, `sum`, `count`, `find`, `forEach`) requests results. ## Intermediate vs terminal - **Intermediate** (return another `Sequence`, lazy): `map`, `filter`, `take`, `drop`, `flatMap`, `distinct`, `sorted` (note: `sorted` is intermediate but *stateful* — it must buffer everything). - **Terminal** (trigger evaluation): `toList`, `first`, `last`, `sum`, `count`, `reduce`, `forEach`, `any`/`all`. ## Why sequences help 1. **No intermediate lists** — a 10-step pipeline over a big list allocates 1 result instead of 10 lists. 2. **Short-circuiting** — `asSequence().map { expensive(it) }.first { it.ok }` stops at the first hit; the eager version maps everything first. 3. **Infinite data** — `generateSequence(1) { it * 2 }.take(10).toList()` is only possible lazily. ```kotlin val powers = generateSequence(1) { it * 2 } // infinite .takeWhile { it < 1000 } .toList() // [1,2,4,...,512] ``` ## When NOT to use a sequence For **small** collections or single cheap operations, eager is simpler and usually faster because sequences add iterator/lambda dispatch overhead per element and per stage. Rule of thumb: reach for sequences when the data is large, the pipeline is long, you can short-circuit, or the source is generated/infinite. ## Multiplatform note `kotlin.sequences` is **common stdlib** — identical semantics on JVM, JS, Native. On JS (no JVM JIT to elide allocations) and on memory-constrained Native targets, the allocation savings from sequences can matter more than on the JVM. Sequences are **single-use** per terminal: iterating one again may recompute or throw for constrained sequences (e.g. `constrainOnce()`).
- Is `sorted()` lazy on a sequence?It returns a sequence (intermediate) but is stateful — it must buffer and sort all elements, so it cannot work on an infinite sequence and defeats some laziness benefits.
- Why might a sequence be slower than a plain list operation?Per-element iterator and lambda-call overhead at each stage; for small inputs that fixed cost outweighs the saved intermediate allocations.
- What makes generateSequence able to model infinite data?It produces elements on demand from a seed and a next-function, so nothing is computed until a terminal op pulls a bounded number of them (e.g. via take/takeWhile).
Eager collections wash all the dishes, then dry all the dishes; a sequence washes-and-dries one plate at a time — and can stop the moment it finds the plate it wanted.
saying these in an interview costs you the question
- Uses asSequence() on tiny lists expecting a speedup
- Thinks intermediate ops like map run immediately on a sequence
- Believes sorted() keeps a sequence fully lazy/infinite-safe
- Forgets a terminal op and wonders why nothing executes
- Reuses a constrainOnce sequence and is surprised it fails