Kotlin Sequences

A sequence is a lazy collection. Regular collection functions process all items at every step. Sequences wait and process items one at a time from start to finish. For large collections with multiple transformation steps, sequences save memory and run faster.

Collections vs Sequences: The Key Difference


Collection (eager):
  listOf(1..1000)
    .filter { it % 2 == 0 }   ← creates full list of 500 evens
    .map { it * 3 }            ← creates full list of 500 results
    .take(5)                   ← then picks first 5

  Total work: 1000 filter + 500 map = 1500 operations

Sequence (lazy):
  (1..1000).asSequence()
    .filter { it % 2 == 0 }
    .map { it * 3 }
    .take(5)
    .toList()

  Processes item by item. Stops as soon as it has 5 results.
  Total work: only ~10 operations

Creating a Sequence

// From a collection
val seq1 = listOf(1, 2, 3, 4, 5).asSequence()

// Using sequenceOf
val seq2 = sequenceOf("A", "B", "C")

// Using generateSequence (infinite)
val naturals = generateSequence(1) { it + 1 }   // 1, 2, 3, 4, ...

// Take the first 10 natural numbers
val firstTen = naturals.take(10).toList()
println(firstTen)   // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Sequence Pipeline


Item enters the pipeline:
  1 → filter(odd?) → YES → map(*10) → 10 → take(3) check
  2 → filter(odd?) → NO  → (skipped entirely)
  3 → filter(odd?) → YES → map(*10) → 30 → take(3) check
  4 → filter(odd?) → NO  → (skipped)
  5 → filter(odd?) → YES → map(*10) → 50 → take(3) DONE

Result: [10, 30, 50]
val result = (1..1000)
    .asSequence()
    .filter { it % 2 != 0 }   // keep odd numbers
    .map { it * 10 }           // multiply by 10
    .take(3)                   // take only 3
    .toList()

println(result)   // [10, 30, 50]

Intermediate vs Terminal Operations


Intermediate (lazy — do nothing until terminal is called):
  filter, map, flatMap, take, drop, sorted, distinct

Terminal (triggers execution and returns a result):
  toList(), toSet(), first(), sum(), count(), forEach()

val seq = (1..10).asSequence().filter { it > 5 }
// Nothing has happened yet!

val list = seq.toList()   // NOW it processes
println(list)             // [6, 7, 8, 9, 10]

Infinite Sequences

// Fibonacci using sequence
val fibonacci = generateSequence(Pair(0, 1)) { Pair(it.second, it.first + it.second) }
    .map { it.first }
    .take(10)
    .toList()

println(fibonacci)   // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

When to Use Sequences


Use regular collections when:
  - Collection has fewer than ~1,000 items
  - You apply only 1-2 transformation steps
  - Simplicity matters more than performance

Use sequences when:
  - Collection is large (thousands or millions of items)
  - You chain 3 or more transformation steps
  - You use take() to stop processing early
  - You work with infinite streams of data

Practical Example: Log File Processor

fun main() {
    val logLines = generateSequence(1) { it + 1 }
        .map { "[$it] ${if (it % 3 == 0) "ERROR" else "INFO"}: Event #$it" }

    // Process only the first 5 ERROR lines without loading everything
    val errors = logLines
        .filter { it.contains("ERROR") }
        .take(5)
        .toList()

    errors.forEach { println(it) }
}

Output:

[3] ERROR: Event #3
[6] ERROR: Event #6
[9] ERROR: Event #9
[12] ERROR: Event #12
[15] ERROR: Event #15

The sequence generates log lines infinitely but stops the moment it finds 5 error lines. No unnecessary computation happens beyond that point.

Leave a Comment

Your email address will not be published. Required fields are marked *