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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kotlin Sequences: Getting Started
25 mins
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.
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. :]
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 File ▸ New ▸ Scratch File.
On the little dialog that pops up, scroll until you find Kotlin, and pick it.
This opens your new scratch file. At the top, you have a few options to play with.
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.
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!
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:
- You check if the previously generated value is below 200,000,000. If so, you add one to it.
- 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.