In this part of the guide, we will explain what coroutines are, how to create them and use some basic coroutine operations. If you did not setup a project that uses Scala Coroutines, please go through the Setup and Dependencies. Otherwise, please proceed with the next section.
Coroutines 101
A coroutine is a programming construct that can suspend its execution and resume execution later. Subroutines (i.e. functions, procedures or methods) are well known and available in most programming languages. For the purposes of this guide, we will jointly refer to functions, procedures, methods and lambdas as subroutines, despite their subtle differences. When a subroutine is called, its execution begins at the start of the subroutine definition, and finishes when the end of the subroutine is reached. A coroutine is more powerful – it can pause execution at any point between the start and the end of the coroutine definition. In this sense, coroutines generalize subroutines.
To better understand how coroutines work, we contrast them to lambdas from functional programming. Consider the following definition of an identity lambda:
val id = (x: Int) => x
We call (i.e. invoke) the identity lambda id
as follows:
id(7)
When id
is invoked, the following sequence of events occurs.
First, the runtime allocates a location on the program stack to hold the parameter x
.
Then, the value of x
is copied to this location.
The id
body then simply returns the value x
back to the caller
by copying it back to the appropriate location.
After that, the invocation of id
(i.e. an instance of the subroutine id
)
completes.
Above, the caller of the id
lambda (e.g. the main program) must suspend execution
until id
completes.
After id
returns control to the caller, the instance of the subroutine
id
no longer exists, so it does not make sense to be able to refer to it.
An invocation (i.e. an instance of a subroutine) is therefore
not a first-class object.
By contrast, a coroutine invocation can suspend its execution before it completes, and later be resumed. For this reason, it makes sense to represent the coroutine invocation (i.e. a coroutine instance) as a first-class object that can be passed to and returned from functions, whose execution state can be retrieved, and which can be resumed when required.
Let’s see the simplest possible coroutine definition – an identity coroutine,
which just returns the argument that is passed to it.
Its definition is similar to that of an identity lambda,
the only difference being the enclosing coroutine
block:
val id = coroutine { (x: Int) => x }
Here, the difference between a coroutine definition
and a coroutine instance becomes more prominent.
We cannot create and get a hold of a coroutine instance
simultaneously with obtaining its return value,
so starting a coroutine is a two step process.
First, a coroutine is instantiated with the call
keyword,
which returns a coroutine instance object.
Second, a coroutine instance is started by calling its resume
method:
val c = call(id(7))
c.resume
assert(c.result == 7)
Above, we first use the call
keyword to create
an id
coroutine instance called c
.
This coroutine instance object c
is created in a suspended state,
and is resumed by calling c.resume
.
To obtain the resulting value from the coroutine, we call c.result
.
The assert
statement is used here to verify that the return value
is really what we expect.
Importantly, the same coroutine definition can be reused to create many coroutine
instances, much like a lambda can be invoked more than once.
A coroutine is defined once by passing a lambda literal
to the special coroutine block.
A coroutine is invoked by passing an invocation expression
to the call keyword,
which returns a fresh coroutine instance.
|
Note that we are explicit about invoking the coroutine id
with the call
construct, to signify that this creates
a fresh coroutine instance.
Also, note that we use the term invocation and instance interchangeably
to refer to starting a coroutine.
The complete, standalone example snippet is shown below:
Although this particular coroutine id
is semantically equivalent
to the prior definition of the identity lambda id
,
its execution is slightly different.
A coroutine instance does not execute on the same program stack as the caller.
Instead, it maintains a separate highly optimized stack,
used to store its execution state,
The id
coroutine is so simple that it does not even suspend execution
after it starts.
The only point when it is suspended
is immediately after its instance is created.
Therefore, this particular coroutine does not even require a state stack.
We will study more complex coroutines next.
Yielding with coroutines
To understand why a separate stack is required,
we will study a more complex coroutine
that takes a string and yields counts of different vowels in that string.
To yield control and a value back to the caller,
coroutines use the special yieldval
statement.
Consider the following example.
Above, we define a coroutine called vowelcounts
,
which takes a String
parameter.
This coroutine then calls the count
method on String
a total of five times,
each time for a different vowel character.
It uses the yieldval
statement five times to suspend execution
and yield the count to the main program.
Each time yieldval
is called, the coroutine execution is paused,
and control is returned to the caller.
The caller then calls resume
on the coroutine instance c
to resume the coroutine.
It is illegal to call resume
on an already completed coroutine instance –
doing this raises an exception.
Summary
To summarize, there are four important coroutine operations you need to remember:
coroutine
– used to define a coroutine, e.g.coroutine { (x: Int) => x }
call
– used to invoke a coroutine, i.e. to start a coroutine instanceresume
– used to resume a paused coroutineyieldval
– used only inside a coroutine to suspend execution and yield a value to the caller
And that’s it – you already know everything you need to get productive with coroutines. You can now start coding, or continue to the next section, where we learn about the coroutine data types.