Welcome back to my series on coroutines. Today we will have a look at the only two higher level functions we get from Kotlin directly:
buildSequence. As one can be implemented calling the other (which is actually done in the original implementations) we will only look into
buildSequence is really just:
Here we also see, that coroutines can get a receiver, which is pretty important for our two functions.
First, let’s have a look at how these functions are used.
As we can see our receiver provides us with two more functions:
yieldAll. These suspend our coroutine until the next value from the sequence/iterator is queried. So all in all we get a lazily generated sequence/iterator. These two are actually pretty similar to generators in Python (which uses a
Here is another practical example with a potentially infinite generator:
yieldAll are the only suspending functions we can call in our coroutine scope. This has two reasons:
- Functions like
delayshown in the previous article require the coroutine context to provide certain features.
SequenceBuilder(our receiver) is annotated with
@RestrictsSuspension, which prevents us from calling any suspending functions other than the ones it provides.
The second one sounds like an arbitrary restriction, but is rather useful considering the first one. For example calling
delay in a coroutine context which does not support it, would result in errors at quite confusing locations. Therefore just telling the developer, that the function cannot be called in this scope is way more helpful.
Actually we can also define custom suspending functions that can be called. These need to be extension functions to
SequenceBuilder though which makes them inherit the restriction. Therefore they also may only call
yieldAll and other extension functions of
From the “Usage” section we can take how our API needs to look and behave. So let’s start with the sequence builder:
As we want the values in our built iterator to be lazily generated we will need some kind of state machine:
So the flow will be like this: Our iterator starts out “Not Ready”. When we call
hasNext the first time, the coroutine would be resumed. If we then called
yield, the state would switch to “Ready”, we would store the yielded value, and
true would be returned by
hasNext. A following call to
next would then return the stored value and switch the state back to “Not Ready”. If we called
hasNext and the coroutine reached its end the state would be switched to “Done” and
hasNext would return
hasNext on a “Done” iterator will then always return
false, while calling
next will throw a
So here is an implementation of the aforementioned concept:
As you can see it was possible to transfer the state machine model to code quite easily. The following implementation of
buildIterator is pretty trivial and mostly equivalent to the original:
Let’s return to the examples from the start. There, the original functions from the Kotlin standard library are imported. Now we’ll see our own implementation in action:
We saw that the functionalities of
buildSequence are quite trivial. We also learned, that they provide an easy way to create lazily generated sequences/iterators. But we also saw their limitations in the context of more complex coroutine features like asynchronity and concurrency. As a more feature complete alternative
kotlinx.coroutines provides us with produce, which returns a
ReceiveChannel. Such a channel is basically a sequence on steroids.