Sequences
Kotlin sequences are lazy — operations are fused into one pass over the data, and short-circuit operators (find, first, any) stop early. For long chains over big inputs, sequences trade per-operation allocations for one final terminal pass.
asSequence, terminal, fused chains, vs list
EXAMPLE
// 1) Eager vs lazy — the key idea
val list = (1..1_000_000).toList()
// EAGER: each step builds a new list of size ~1M
val eagerResult = list
.map { it * 2 } // allocates 1M
.filter { it % 3 == 0 } // allocates ~330k
.take(5)
.toList()
// LAZY: nothing materialises until 'take(5).toList()' pulls 5 values
val lazyResult = list.asSequence()
.map { it * 2 }
.filter { it % 3 == 0 }
.take(5)
.toList() // pulls only as many source elements as needed
// Sequences shine when:
// • Chain is long
// • Input is large
// • Terminal op is short-circuiting (take, first, find, any)
// 2) Building a sequence
val s1 = sequenceOf(1, 2, 3, 4, 5)
val s2 = generateSequence(1) { it * 2 } // 1, 2, 4, 8, 16, 32... infinite!
.take(10)
.toList() // [1,2,4,8,16,32,64,128,256,512]
val s3 = generateSequence(::nextLine) // sequence of lines from stdin until null
val s4 = sequence {
yield(1)
yield(2)
yieldAll(listOf(3, 4))
var n = 100
while (n < 110) {
yield(n)
n++
}
}
println(s4.toList()) // [1, 2, 3, 4, 100, 101, …, 109]
// 3) Conversion
listOf(1, 2, 3).asSequence()
"hello".asSequence().toList() // [h, e, l, l, o]
mapOf("a" to 1).asSequence().toList()
// 4) Intermediate operations (LAZY)
val seq = (1..10).asSequence()
.filter { it % 2 == 0 }
.map { it * 10 }
.flatMap { listOf(it, -it).asSequence() }
.distinct()
.sorted() // SORTING IS NOT LAZY — needs full pass
.take(5)
// Lazy ops you can use: filter, filterNot, map, flatMap, distinct, take, drop, takeWhile, dropWhile,
// zip, zipWithNext, plus, onEach
// 5) Terminal operations (materialise)
seq.toList()
seq.toSet()
seq.toMap()
seq.forEach { println(it) }
seq.count()
seq.sum()
seq.max() // requires full pass
seq.first() // short-circuit
seq.find { it > 5 } // short-circuit
seq.any { it > 5 } // short-circuit
seq.all { it > 0 } // short-circuit on first false
seq.fold(0) { acc, x -> acc + x }
seq.reduce { acc, x -> acc + x }
seq.joinToString(", ")
// 6) When eager wins
// • Short chains — extra Sequence wrapping costs more than the saved allocation
// • Small inputs — overhead dominates
// • Sort, distinct, group — need full pass anyway
// • Indexed access by position
// Rule of thumb: 4+ stages OR 100k+ elements + short-circuit terminal → consider sequence
// 7) Real-world: process lines of a big file lazily
import java.io.File
File("huge.log").useLines { lines ->
val total = lines
.filter { it.contains("ERROR") }
.map { it.length }
.sum()
println(total)
}
// useLines passes a Sequence; the file is streamed line by line.
// No way to hold the whole file in memory.
// 8) Combining sequences
val evens = generateSequence(0) { it + 2 }
val odds = generateSequence(1) { it + 2 }
evens.zip(odds).take(5).toList() // [(0,1), (2,3), (4,5), (6,7), (8,9)]
evens.zipWithNext().take(3).toList() // [(0,2), (2,4), (4,6)]
val merged = sequenceOf(1, 3, 5) + sequenceOf(2, 4, 6)
println(merged.toList()) // [1, 3, 5, 2, 4, 6]
// 9) Re-iteration
val seq = (1..3).asSequence()
seq.toList() // [1, 2, 3]
seq.toList() // also [1, 2, 3] — most sequences are re-iterable
// But generateSequence + sequence{} are CONSTRAINED to one iteration each:
val once = generateSequence { readLine() } // one-shot — exhausted after first traversal
// Cache for repeated iteration:
val cached = once.toList()
// 10) Sequence vs Flow (kotlinx.coroutines)
// • Sequence: synchronous, eager source, lazy ops
// • Flow: asynchronous, cold, suspendable — for async streams (network, DB)
// Use Sequence for sync data; Flow for IO / async producers.
// 11) Pitfalls
// • Calling .toList() too early in the chain — kills laziness
// • Sorting a million-element sequence — it materialises; can't be lazy by definition
// • Sequence on a Channel-backed stream — use Flow with proper concurrency
// • Side effects via onEach in lazy chain — runs only when terminal pulls
// • Re-iterating a one-shot sequence → silent empty result
// 12) Benchmarking
fun benchmark(name: String, block: () -> Unit) {
val start = System.nanoTime()
block()
val ms = (System.nanoTime() - start) / 1_000_000
println("$name: ${ms} ms")
}
benchmark("list") { (1..1_000_000).toList().map { it * 2 }.filter { it % 3 == 0 }.take(5) }
benchmark("seq") { (1..1_000_000).asSequence().map { it * 2 }.filter { it % 3 == 0 }.take(5).toList() }
// Always measure for your real shape; tiny pipelines may not benefit from sequences.
// 13) Common bugs
// • Treating a sequence as a list — index access slow, toList() everywhere defeats purpose
// • Capturing state in lambdas across lazy chain — runs at terminal time; surprising mutation order
// • Long-running sequences without cancellation — wrap with a CancellationException check if interruptible
// • Sequence + parallel ops — sequences are sequential; use Flow + parallel collectors
// • Forgetting that sorted() etc. break laziness
// • Iterating an exhausted one-shot sequence and getting nothing
Why it matters
Sequences are lazy: chain .asSequence() + intermediate ops + a short-circuiting terminal, and Kotlin pulls only as many source elements as the terminal needs. They’re a win for long pipelines, big data, and take/first/find/any; for small inputs or operations that need a full pass (sorted, distinct), regular collections are simpler.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
(1..1_000_000)
.asSequence()
.map { it * 2 }
.filter { it % 3 == 0 }
.take(5)
.toList()
Try it Yourself »
Discussion
Loading…