iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…