Kotlin Sequences: Getting Started

In this Kotlin Sequences tutorial, you’ll learn what a sequence is, its operators and when you should consider using them instead of collections. By Ricardo Costeira.

5 (4) · 1 Review

Download materials
Save for later
Share

Dealing with multiple items of a specific type is part of the daily work of, most likely, every software developer out there. A list of coffee roasters, a set of coffee origins, a mapping between coffee origins and farmers… It really depends on the use case.

You can handle this kind of data in a few ways. The most common is through the Collections API. For instance, translating the cases above, you could have something like List<Roaster>, Set<Origin> or Map<Origin, Farmer>.

While the Collections API does a good job, it might not be suited for all cases. It’s always useful to be aware of alternatives, how they work, and when they can be a better fit.

In this tutorial, you’ll learn about Kotlin’s Sequences API. Specifically, you’ll learn:

  • What a sequence is and how it works.
  • How to work with a sequence and its operators.
  • When should you consider using sequences instead of collections.
Note: This tutorial assumes you have basic Kotlin knowledge. If not, check out Programming in Kotlin first.

Getting Started

Download the project materials by clicking the Download Materials button at the top or bottom of this tutorial, and open the starter project.

Run the project, and you’ll notice it’s just a simple “Hello World” app. If you came here hoping to implement some cool app full of sequences everywhere, the sad truth is that you won’t even touch the app’s code. :]

A simple "Hello World" app on a phone screen.

Instead, the project exists just so you can use it to create a scratch file. When working on a project, you may want to test or draft some code before actually proceeding to a proper implementation. A scratch file lets you do just that. It has both syntax highlighting and code completion. And the best part is, it can run your code right after you write it, letting you debug it as well!

You’ll now create the scratch file where you’ll work. In Android Studio, go to FileNewScratch File.

Creating a new scratch file by selecting it from the dropdown menu.

On the little dialog that pops up, scroll until you find Kotlin, and pick it.

In your case, the position may be different.

Selecting the kind of scratch file from the dialog.

This opens your new scratch file. At the top, you have a few options to play with.

Scratch file options.

Make sure Interactive mode is checked. This runs any code you write after you stop typing for two seconds. The Use classpath of module option is pretty useful if you want to test something that uses code from a specific module. Since that’s not the case here, there’s no need to change it. Also, make sure to leave Use REPL unchecked, as that would run the code in Kotlin REPL, and there’s no need for that here.

Look at your project structure, and you’ll notice that the scratch file is nowhere to be seen. This is because scratch files are scoped to the IDE rather than the project. You’ll find the scratch file by switching to the Project view under Scratches and Consoles.


Selecting scratch.kts in Scratches and Consoles.

This is useful if you want to share scratch files between different projects, for example. You can move it to the project’s directory, but that’s not relevant for what you’ll do in this tutorial. That said, it’s time to build some sequences!

Note: If you want to know more about scratch files, check the Jetbrains documentation about them.

Understanding Sequences

Sequences are data containers, just like collections. However, they have two main differences:

  • They execute their operations lazily.
  • They process elements one at a time.

You’ll learn more about element processing as you go through the tutorial. For now, you’ll dig deeper into what does it mean to execute operations in a lazy fashion.

Lazy Processing

Sequences execute their operations lazily, while collections execute them eagerly. For instance, if you apply a map to a List:

val list = listOf(1, 2, 3)
val doubleList = list.map { number -> number * 2 }

The operation will execute immediately, and doubleList will be a list of the elements from the first list multiplied by two. If you do this with sequences, however:

val originalSequence = sequenceOf(1, 2, 3)
val doubleSequence = originalSequence.map { number -> number * 2 }

While doubleSequence is a different sequence than originalSequence, it won’t have the doubled values. Instead, doubleSequence is a sequence composed by the initial originalSequence and the map operation. The operation will only be executed later, when you query doubleSequence about its result. But, before getting into how to get results from sequences, you need to know about the different ways of creating them.

Creating a Sequence

You can create sequences in a few ways. You already saw one of them above:

val sequence = sequenceOf(1, 2, 3)

The sequenceOf() function works just like the listOf() function or any other collections function of the same kind. You pass in the elements as parameters, and it outputs a sequence.

Another way of creating a sequence is by doing so from a collection:

val coffeeOriginsSequence = listOf(
  "Ethiopia", 
  "Colombia", 
  "El Salvador"
).asSequence()

The asSequence() function can be called on any Iterable, which every Collection implements. It outputs a sequence with the same elements present in said Iterable.

The last sequence creation method you’ll see here is by using a generator function. Here’s an example:

val naturalNumbersSequence = generateSequence(seed = 1) { previousNumber -> previousNumber + 1 }

The generateSequence function takes a seed as the first element of the sequence and a lambda to produce the remaining elements, starting from that seed.

Unlike the Collection interface, the Sequence interface doesn’t bind any of its implementations to a size property. In other words, you can create infinite sequences, which is exactly what the code above does. The code starts at one, and goes to infinity and beyond from there, adding one to each generated value.

As you might suspect, you could get in trouble if you try to operate on this sequence. It’s infinite! What if you try to get all its elements? How will you stop?

One way is to use some kind of stopping mechanism in the generator function itself. In fact, generateSequence is programmed to stop generation when it returns null. Translating that into code, this is how to create a finite sequence:

val naturalNumbersUpToTwoHundredMillion = 
  generateSequence(seed = 1) { previousNumber ->
    if (previousNumber < 200_000_000) { // 1
        previousNumber + 1
    } else {
        null // 2
    }
  }

In this code:

  1. You check if the previously generated value is below 200,000,000. If so, you add one to it.
  2. If you reach a value equal to 200,000,000 or above, you return null, effectively stopping the sequence generation.

Another way of stopping sequence generation is by using some of its operators, which you'll learn about in the next section.