Lazy Evaluation
element-at-a-time Β· no intermediate collectionsA List operator chain runs each step to completion, front to back, allocating a full intermediate list between every step. A Sequence chain instead processes one element through the entire chain of operations before moving to the next element β no step waits for the previous one to finish the whole collection first. This single change in evaluation order is the entire idea behind sequences; everything else on this page follows from it.
List (eager): step-by-step over the WHOLE collection
[1,2,3,4,5] --filter--> [2,4] --map--> [4,16] (2 full passes,
(full pass) (full pass) 2 intermediate lists)
Sequence (lazy): element-by-element through the WHOLE chain
1 --filter(fail)-------------------------------X
2 --filter(pass)--> --map--> 4 ---------------> collected
3 --filter(fail)-------------------------------X
4 --filter(pass)--> --map--> 16 --------------> collected
5 --filter(fail)-------------------------------X
(1 pass total, no intermediate list β each element flows straight
through filter then map before the next element is even touched)
filter/map on it does no work whatsoever by itself. This is the same "cold" idea that shows up again with Flow: defining the pipeline and running it are two separate steps.asSequence
List β Sequence Β· one-time conversionThe most common way to get a Sequence is converting an existing collection with .asSequence(). This is a thin, near-free wrapper β it doesn't copy the underlying data, just changes how iteration over it behaves for whatever chain follows.
val numbers = (1..1_000_000).toList() // Eager: allocates a full ~500,000-element list after filter, // then a full list again after map, before take(3) ever runs val eagerResult = numbers .filter { it % 2 == 0 } .map { it * it } .take(3) // Lazy: only as many elements as needed to satisfy take(3) are // ever pulled through filter and map β evaluation stops early val lazyResult = numbers.asSequence() .filter { it % 2 == 0 } .map { it * it } .take(3) .toList() // terminal operation: triggers the actual work println(lazyResult) // [4, 16, 36]
generateSequence
infinite sequences Β· seed + nextgenerateSequence builds a sequence from a seed value and a function computing the next element from the previous one, with no predetermined end β the sequence can be genuinely infinite, since nothing is computed until something downstream asks for it. This is something a List fundamentally can't represent (a list has a size, known up front); it's a place sequences do something collections structurally cannot, not just something they do more efficiently.
// Powers of two: 1, 2, 4, 8, ... with no upper bound defined val powersOfTwo = generateSequence(1) { it * 2 } // take() is what makes this safe: only 5 elements are ever computed println(powersOfTwo.take(5).toList()) // [1, 2, 4, 8, 16] // A seed function instead of a fixed value, for lazy/random starts val randomWalk = generateSequence(seedFunction = { 0 }) { it + (-1..1).random() } println(randomWalk.take(5).toList()) // A nullable next function defines its own stopping point instead val untilNull = generateSequence(1) { if (it < 5) it + 1 else null } println(untilNull.toList()) // [1, 2, 3, 4, 5]: stops once the next function returns null
.toList() or any other unbounded terminal operation directly on a generateSequence with no natural stopping condition never returns β there's nothing to stop it. Always pair a truly infinite sequence with take(n), takeWhile { }, or a next function that eventually returns null.Intermediate vs Terminal Operations
lazy Β· eager Β· toList / sum / firstEvery operation in a sequence chain is one of two kinds. Intermediate operations (filter, map, take, drop) are themselves lazy β calling one just records a step in the pipeline and returns another unevaluated Sequence, doing no actual work. Terminal operations (toList, sum, first, forEach, count) are what actually pulls elements through the whole pipeline, and there's exactly one terminal operation per chain, always the last call.
val pipeline = listOf(1, 2, 3, 4, 5).asSequence() .map { println("mapping $it") it * it } .filter { println("filtering $it") it > 5 } // Nothing printed yet β map and filter are intermediate, not evaluated val result = pipeline.first() // terminal: pulls elements one at a time until one matches // mapping 1 // filtering 1 // mapping 2 // filtering 4 // mapping 3 // filtering 9 β 9 > 5: first() stops here, never touches 4 or 5 println(result) // 9
When Sequences Win, When They Don't
decision guide Β· measure firstfirst, find, any, take(n)) means most of the collection never needs processing at all β the first() example above only touched 3 of 5 elements. Lists win for short chains over small collections, where the sequence machinery's own iterator overhead costs more than the intermediate lists it's avoiding, and for anything needing random access or a known size up front, which sequences don't provide.| Situation | Prefer | Why |
|---|---|---|
| Large collection, several chained operators | Sequence | Avoids one full intermediate list per step |
| Short-circuiting terminal op (first, take(n), any) | Sequence | Stops early; may never process most elements |
| Small collection, one or two operators | List | Sequence overhead outweighs the savings |
| Need the size upfront, or random access by index | List | Sequence has neither without a terminal op |
| Infinite or unbounded generation | Sequence | Lists can't represent an unbounded collection at all |
Quick Reference
Syntax cheat-sheet| Concept | Syntax | Notes |
|---|---|---|
| From a collection | list.asSequence() | Thin wrapper, no data copy |
| Fixed values | sequenceOf(1, 2, 3) | Direct sequence construction |
| Infinite from seed | generateSequence(1) { it * 2 } | Must pair with take/takeWhile to terminate |
| Bounded by null | generateSequence(1) { if (...) it + 1 else null } | Stops when the next function returns null |
| Intermediate operation | filter / map / take / drop | Lazy: records a step, does no work yet |
| Terminal operation | toList / sum / first / forEach / count | Triggers evaluation of the whole pipeline |
| Large chain, large data | prefer Sequence | No per-step intermediate collection |
| Small chain, small data | prefer List | Sequence's iterator overhead isn't worth it |