Sequence

  • Sequences:
    • Lazy
    • Evaluated on demand
      • Do not need to create collections at every step
      • Keep the natural order of operations.
    • More memory-efficient

img

Sequences are lazy, therefore intermediate functions for Sequence processing don’t do any calculations. Instead, they return a new Sequence that decorates the previous one with the new operation. All these computations are evaluated during a terminal operation like toList() or count() - Effective Kotlin

The sequence builder should NOT use suspending operations other than yielding operations.

Notes

Keep the natural order of operations.

Code

fun main() = runBlocking {
  sequenceOf(1, 2, 3)
    .filter { out.printBlue("F:$it, "); it % 2 == 1 }
    .map { out.printGreen("M:$it, "); it.toString() + "a"}
    .forEach { out.print("E:$it, ") }
}

Command to reproduce:

gt.sandbox.checkout.commit 8b58878d5aa38a9ea4e1 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

F:1, M:1, E:1a, F:2, F:3, M:3, E:3a, 

Do the Minimal Number of Operations

Often we do not need to process a whole collection at every step to produce the result. Let’s say that we have a collection with millions of elements and, after processing, we only need to take the first 10. Why process all the other elements? Iterable processing doesn’t have the concept of intermediate operations, so a processed collection is returned from every operation. Sequences do not need that, therefore they can do the minimal number of operations required to get the result. - Effective Kotlin

Code example: where with Sequence we process less functions

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.Out
import gt.sandbox.util.output.impl.OutSettings

val out = Out.standard(OutSettings(printCoroutineName = false))

fun <T> T.printlnGreen() {
  out.printGreen(this.toString())
  println()
}

fun main(args: Array<String>) {

  println("As list - finding a match:")

  (1..10)
    .filter { out.printBlue("F$it, "); it % 2 == 1 }
    .map { print("M$it, "); it * 2 }
    .find { it > 5 }
    .printlnGreen()

  println()
  println()
  println("As sequence - finding a match:")
  (1..10).asSequence()
    .filter { out.printBlue("F$it, "); it % 2 == 1 }
    .map { print("M$it, "); it * 2 }
    .find { it > 5 }
    .printlnGreen()
}

Command to reproduce:

gt.sandbox.checkout.commit 318c2014d368df941520 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

6 is the match is the first match in this case:

As list - finding a match:
F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, M1, M3, M5, M7, M9, 6


As sequence - finding a match:
F1, M1, F2, F3, M3, 6

For this reason, when we have some intermediate processing steps and our terminal operation does not necessarily need to iterate over all elements, using a sequence will most likely be better for your processing performance and it looks nearly the same. Examples of operations that do not necessarily need to process all the elements are [first, find, take, any, all, none, or indexOf].

Simple sequence showing on demand

Shows that only the required elements are evaluated from the sequence.

GT-Sandbox-Snapshot

Code

package com.glassthought.sandbox

import kotlinx.coroutines.runBlocking

val seq = sequence {
  println("Generating first")
  yield(1)
  println("Generating second")
  yield(2)
  println("Generating third")
  yield(3)
  println("Done")
}

fun main() = runBlocking {
  println("Final output: " + seq.take(2).toList())
}

Command to reproduce:

gt.sandbox.checkout.commit f3e001677ad7af65443f \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

Generating first
Generating second
Final output: [1, 2]

Can Be Infinite.

Thanks to the fact that sequences do processing on demand, we can have infinite sequences. A typical way to create an infinite sequence is using sequence generators like generateSequence or sequence.

generateSequence example

Code

package com.glassthought.sandbox

import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
  val seedValue = 1
  
  generateSequence(seedValue) { it + 1 }
    .take(10)
    .forEach { print("$it, ") }
}

Command to reproduce:

gt.sandbox.checkout.commit 262033ba438658622981 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 
Fibonacci example: using 'sequence'

Code

package com.glassthought.sandbox

import gt.sandbox.util.output.print
import gt.sandbox.util.output.printGreen
import gt.sandbox.util.output.println
import kotlinx.coroutines.runBlocking
import java.math.BigDecimal


// Declare a sequence of Fibonacci numbers, using BigDecimal
// to handle large values.
val fibonacci: Sequence<BigDecimal> = sequence {
  // Initialize the first two Fibonacci numbers.
  var current = 1.toBigDecimal()  // Current number in the sequence
  var prev = 1.toBigDecimal()     // Previous number in the sequence

  // Emit the first Fibonacci number (1).
  yield(prev)

  // Use an infinite loop to generate subsequent numbers.
  while (true) {
    // Emit the current Fibonacci number.
    yield(current)

    // Compute the next Fibonacci number by adding the previous two numbers.
    val temp = prev        // Temporarily store the previous number
    prev = current         // Update the previous number to the current one
    current += temp        // Update the current number to the sum of current and previous
  }
}

fun main() = runBlocking {
  fibonacci.take(10).toList().println()

  System.out.flush()
}

Command to reproduce:

gt.sandbox.checkout.commit d48a6563eadebbd18ccf \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew run --quiet"

Recorded output of command:

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

References


Children
  1. Kotlin Sequences: Evaluated on Demand
  2. Sequence Examples