Drako's Developer Blog

My thoughts on programming

Introduction to Coroutines – Part 2

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: buildIterator and buildSequence. As one can be implemented calling the other (which is actually done in the original implementations) we will only look into buildIterator.

buildSequence is really just:

Here we also see, that coroutines can get a receiver, which is pretty important for our two functions.

Usage

First, let’s have a look at how these functions are used.

As we can see our receiver provides us with two more functions: yield and 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 yield keyword).

Here is another practical example with a potentially infinite generator:

Restrictions

Sadly yield and yieldAll are the only suspending functions we can call in our coroutine scope. This has two reasons:

  • Functions like delay shown 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 yield, yieldAll and other extension functions of SequenceBuilder.

Implementation

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 false. Calling hasNext on a “Done” iterator will then always return false, while calling next will throw a NoSuchElementException.

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:

Working example

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:

See also

We saw that the functionalities of buildIterator and 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.


Leave a Reply

Your email address will not be published. Required fields are marked *

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close