DEV Community

Zach Klippenstein
Zach Klippenstein

Posted on

What is restricted suspension in Kotlin?

Why can't I call suspend functions in sequence?

sequence lets you create sequences using a syntax that looks kind of like Python generators, using a feature of coroutines that you don’t normally run into when you’re using coroutines for threading or normal async programming.

Be careful not to confuse the coroutine-based sequence builder, sequence, with its more functional sibling, generateSequence.

What does "Kotlin Coroutines" even mean?

Typically when you think coroutines, you’re actually thinking coroutines + jobs + dispatchers + etc. But dispatchers and even jobs are not part of the core coroutine support in Kotlin. Most of the coroutine goodies are actually in a completely separate library: kotlinx.coroutines. The core coroutine support in Kotlin is basically just the Continuation type, a low level API to get a continuation out of a suspend function, and very low-level support for CoroutineContexts. A continuation is basically just a callback that the compiler generates for you from the body of a suspend function. The stdlib is not opinionated about what kinds of things you can build with these fundamentals. They can be useful for general async multithreaded programming - that’s what kotlinx.coroutines is for – but they can also do other tricks.

You’ll notice that sequence is in the stdlib and doesn’t use anything from kotlinx.coroutines.

Laziness

The sequence builder doesn’t create a general coroutine scope that would, e.g., allow you to go off and do other work in a thread. If that’s what you want then you don’t want a Sequence, you want a Flow. A sequence is simply a regular Iterable (yep, the one from Java) that just has the contract of calculating itself lazily (something iterables can do, but usually don’t - see List, Set, etc). The sequence builder doesn’t want to let you do just any asynchronous work. Imagine if it did - if you awaited a Deferred, for example, you’d need to potentially block the thread to wait for the result to be available. But all that blocking and await stuff is specific to the kotlinx.coroutines library. The sequence builder needs to be very strict - the only type of asynchrony it supports is to suspend until the next call to next on its Iterator – it doesn't know how to schedule arbitrary coroutines. Yield is the only suspend function it supports because the only reason to suspend is to wait for the next element of the sequence to be requested.

Concurrency vs Parallelism

No exposition about coroutines would be complete without reminding the reader that concurrency ≠ parallelism. Parallelism means two things are happening at the same actual time. When you blink both eyes at the same time, that's parallelism. Concurrency, on the other hand, is more subtle, and simply means that the ordering of two events (or sequences of events) is undefined. It's kind of like merging onto a freeway – you have to enter between two vehicles, but it doesn't matter which two.

The sequence coroutine is concurrent, but not parallel, because while its code is interleaved with the code pulling from the sequence, the two never actually execute at the same time.

The sequence of a sequence

The coordination between a sequence coroutine and its Sequence is much tighter than you usually see with kotlinx.coroutines code. The call to yield indicates a new value is produced, and thus must provide that value to the sequence builder. It wouldn’t make sense to suspend any other way, since the sequence builder needs to know the next value on each suspension. When you pull from a Sequence, here's what the sequence builder does:

  1. It doesn’t do anything until the first sequence element is requested via Iterator.next().
  2. When requested, it runs the coroutine until it suspends. Because the only way to suspend a sequence coroutine is to provide a value with yield, the suspension is guaranteed by the compiler to provide the requested value.
  3. Returns the yielded value from next(), then goes back to 1.

Actually next and hasNext can both resume the coroutine, I just used next for brevity.

So the only way to provide an element is to suspend, and the only reason to suspend is to provide an element. But suspend is just a function modifier – does this mean that sequence has to throw at runtime if you try hopping threads?

Restricted suspension

The core coroutine support in the Kotlin stdlib provides a special annotation that can coroutine builders can use to restrict the types of suspension that can be done in their coroutines: @RestrictsSuspension. It works by only allowing the suspend functions defined on the specified receiver type to be invoked. Calling any other suspend functions gives a compiler error.

The sequence builder's lambda has a receiver type of SequenceScope. This class is annotated with @RestrictsSuspension:

@RestrictsSuspension
@SinceKotlin("1.3")
public abstract class SequenceScope<in T> // …
Enter fullscreen mode Exit fullscreen mode

You can use @RestrictsSuspension for other things too, in your own code - any time you want to be very specific about what it means to “suspend” a particular coroutine.

Top comments (0)