Sequences

Sequence allows to perform all the processing steps one-by-one for every single element.

The sequence itself does not store elements.

You can use the same operations on a sequence as on a collection, such as map, filter, etc.

There are two types of operations:

  • intermediate operations - define processing steps. In fact, each step is a sequence with a predicate to be used in the traversal. For example, map() and filter() are intermediate operations.
  • terminal operations - define last processing step. All intermediate operations will be invoked for each element. As a result, the operation may produce a single value or a new collection. Obviously, only one terminal operation can be used. For example, count() and toList() are terminal operations.

Sequence is an alternative to Java stream.

fun fibonacci() = sequence {
    var terms = Pair(0, 1)

    // this sequence is infinite
    while (true) {
        yield(terms.first)
        terms = Pair(terms.second, terms.first + terms.second)
    }
}

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

creation

You can create sequence from any Iterable or Iteratorobject.

val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()
val numbersSequence2 = (1..10).asSequence()

val result = myIterator.asSequence().filter {
           // todo
        }.filter {
            // todo
        }.toList()

You can create sequence from elements.

val numbersSequence = sequenceOf("four", "three", "two", "one")

You can generate sequence elements with a given function. It looks like math sequence.

val oddNumbers = generateSequence(1) { it + 2 } // `it` is the previous element
println(oddNumbers.take(5).toList())
//println(oddNumbers.count())     // error: the sequence is infinite

You can create sequence from chunks.

val oddNumbers = sequence {
    yield(1)
    yieldAll(listOf(3, 5))
    yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList())

sequence vs collections

Sequences let you avoid building results of intermediate steps, therefore improving the performance of the whole collection processing chain.

dstCollection = srcCollection
                    .filter { /* ... */  }    // new collection will be created
                    .map { /* ... */ }        // new collection will be created
                    .filter { /* ... */ }     // new collection will be created

dstCollection = srcCollection
                    .asSequence()
                    .filter { /* ... */ }    // declare step 
                    .map { /* ... */ }       // declare step 
                    .filter { /* ... */ }    // declare step 
                    .toList()                // new collection will be created

sequence vs Java stream

Sequence advantages:

  • available on all Kotlin platforms, not only JVM
  • most terminal operations are inline
  • more specified operations available like mapNotNull
  • built-in aggregation operations like groupBy more concise
  • the Kotlin null safety features are used. Java uses Optional type to wrap null values.

Stream advantages:

  • have primitive variants to avoid unnecessary autoboxing
  • enables easy parallelism via parallel streams