Sequence builders in Kotlin Coroutines
- async functions (also called async/await),
- generator functions (the functions, where next values are yielded).
We've already seen how
async can be used in Kotlin, but it will be explained in detail in the chapter Coroutine builders. Instead of generators, Kotlin provides a sequence builder - a function used to create a sequence1.
A Kotlin sequence is a similar concept to a collection (like
Set), but it is evaluated lazily. Meaning, the next element is always calculated on-demand, when it is needed. As a result, sequences:
- do the minimal number of required operations,
- can be infinite,
- are more memory-efficient.
Due to those characteristics, it makes a lot of sense to define a builder, where next elements are calculated and "yielded" on demand. We define it using function
sequence. Inside its lambda expression, we can call the
yield function, to produce next elements of this sequence.
sequencefunction here is a small DSL. Its argument is a lambda expression with a receiver, which means that it changes the meaning of the
thisreferences to, is called the receiver. In this case, its type is
SequenceScope, and it has functions like
yield. When we call
yield(1), it is equivalent to calling
thiscan be used implicitly. This will make more sense as we will observe it over and over again in the book.
What is essential here, is that each number is generated on demand, not in advance. You could observe this process well if we print something on both the builder, and on the place where we handle our sequence.
Let's analyze how it works. We ask for the first number, so we enter the builder. We print "Generating first", and we yield number 1. Then it is handled, and so "Next number is 1" is printed. Then something crucial happens - we jump with execution to the place where we previously stopped, to find another number. This would be impossible without a suspension mechanism, as it wouldn't be possible to stop a function in the middle, and resume it from the same point later in the future. Thanks to suspension, we can do this, we can freely jump with execution between
main and the sequence generator.
To see it more clearly, let's manually ask for a few values from the sequence.
Here we used an iterator to get the next values. At any point, we can call it again, to jump in the middle of the builder function, and generate the next value. Would it be possible without coroutines? Maybe, if we would dedicate a thread for that. Such thread would need to be maintained, and that would be a huge cost. With coroutines, it is fast and simple. Moreover, we can keep this iterator for as long as we wish to as it costs nearly nothing. Soon we will learn how this mechanism works under the hood (in the chapter Suspension under the hood).
Here are a few ways how a sequence builder can be used. The most typical one is to generate a mathematical sequence. For instance, a Fibonacci sequence.
We've learned about the sequence builder, and why it needs suspension to work correctly. We've seen suspension in action, it is time to dive even deeper to understand how suspension works when we use it directly.
Even better, it offers flow builders. Flow is a similar, but much more powerful concept, which we will explain later in the book.