Kotlin Sequences

Tompee Balauag
Familiar Android
Published in
3 min readJun 5, 2018
Photo by Jan Gottweiss on Unsplash

This article is a counterpart of the Java 8 streams article. I highly suggest you read this first to gain more context and for a smooth learning experience.

Kotlin sequences are similar to Java 8 streams. They are lazily evaluated collections that allows for vertical processing. Why do we even need sequences in the first place, when collections can be declarative in kotlin? Let’s look at a familiar example.

This code takes a list from 1 to 5 and maps it to itself while printing its current value. The first takes the first element of the list and returns it. Running this will output the following

1
2
3
4
5
result: 1

This is all well and good but notice that this is not efficient. There is no need to iterate over the entire list just to return the first element. Sequence solves this problem. Before we delve to the examples, let’s define the properties and similarities of sequences and Java streams.

Declaring sequences

There are many ways to declare sequences. Lists and maps can be converted via the asSequence method. Iterator can also be “wrapped” as a sequence. sequenceOf can also be used.

[1] listOf<String>().asSequence()
mapOf<String, String>().asSequence()
[2] Sequence { listOf<String>().listIterator() }
[3] sequenceOf(1,2,3)

Sequence Operations

There are 2 types of sequence operations, intermediate and terminal.

Intermediate operators, similar to Java Streams intermediate operators, are lazy operations. It creates a new sequence as output.

The code above converts a list to sequence and performs bitwise and. The result is below.

result: kotlin.sequences.TransformingSequence@cac736f

The result is of type Sequence (as expected) and the map operations are not logged (as expected as well because of laziness). Take note that a sequence is not stored in memory. Invoking operations on sequence will recalculate each element.

Terminal operations evaluate sequences. They iterate over the sequence and terminates them if a condition is satisfied. Let’s take a look at the first operator we encountered above.

This code is similar to the first example, but using sequence instead of list. The output of the code is below.

1
result: 1

Look at how the unnecessary iterations are solved. This is because sequences allow for vertical processing. This will be discussed further in sequence operation classification.

Operator Classification

Similar to Java 8 Streams, operations are classified into two, stateless and stateful.

Stateless operations requires no knowledge of the sequence. In other words, the operation can process the sequence items independently. This property allows for vertical processing. Examples of stateless operations are map, filter and take.

The function above converts a list from 1 to 10 to sequence, maps it to the same value, filters anything equal or less than 5, takes only 3 elements and converts to list. The output of this is below.

mapping 0
filtering 0
mapping 1
filtering 1
mapping 2
filtering 2
mapping 3
filtering 3
mapping 4
filtering 4
mapping 5
filtering 5
mapping 6
filtering 6
mapping 7
filtering 7
mapping 8
filtering 8
result: [6, 7, 8]

Notice that all operations (aside from toList) are performed for each element in succession. This is what vertical processing means.

Now let’s take a look at stateful operations. Stateful operations require knowledge of the sequence before it can be evaluated. This properly is what forces horizontal processing. Examples of stateful operations are toList and sorted.

We peak at the elements before and after sorted to diagnose the order of elements that are processed. Below is the output.

before sorted: 10
before sorted: 9
before sorted: 8
before sorted: 7
before sorted: 6
before sorted: 5
before sorted: 4
before sorted: 3
before sorted: 2
before sorted: 1
before sorted: 0
after sorted: 0
after sorted: 1
after sorted: 2
after sorted: 3
after sorted: 4
after sorted: 5
after sorted: 6
after sorted: 7
after sorted: 8
after sorted: 9
after sorted: 10
result: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

As expected, the original sequence was exhausted first before sorting. This is because when sorting, you need to compare all the elements to be able to output a sorted list. This is horizontal processing in a nutshell.

Why Sequence?

Now the question, why Sequence when there is Iterable? Well, as we have said before, sequences are lazy and allows for vertical processing. It is good for distributing the process from the terminal operations. And some sequences can be iterated multiple times.

--

--