Understanding coroutines

An overview of the relationship between normal functions, coroutines and streams.

Previous post: Async generators.

Introduction

In some other posts on this site, I discuss how to create streams from scratch and how to combine them into new aggregated streams. This post puts them in perspective in relation with the other common types of functions you already know.

Coroutines

Conceptual

Normal functions just take input and return output (immediately).

Coroutines are functions that can be suspended.

  1. Upon being suspended a coroutine yields a value.
  2. Then the caller can continue with other functions.
  3. Later the caller may decide to resume the suspended coroutine with resumption data input
  4. If the coroutine ends, it returns (not yield) a final value

Implicit coroutines

Coroutines are used internally by the compiler when creating state machines from async blocks.

Directly constructing coroutines

You may want to construct a coroutine yourself. However, the Coroutine trait in Rust is unstable and only available on nightly as of April 25.

The Coroutine trait definition is an extension of the standard libraries Future trait:

pub trait Coroutine<R = ()> {
    type Yield;
    type Return;

    fn resume(
        self: Pin<&mut Self>,
        arg: R,
    ) -> CoroutineState<Self::Yield, Self::Return>;
}

Notice the R stands for Resume, is a type generic and is different from Coroutine::Return.

This means that one same type may behave as different coroutines depending on the resume input, but can only have one Return type.

Example of Rust coroutine

The following coroutine (in nightly Rust) has resume data type R = ():

let mut coroutine = #[coroutine] || {
    yield 1;
    "foo"
};

match Pin::new(&mut coroutine).resume(()) {
    CoroutineState::Yielded(1) => {}
    _ => panic!("unexpected return from resume"),
}
match Pin::new(&mut coroutine).resume(()) {
    CoroutineState::Complete("foo") => {}
    _ => panic!("unexpected return from resume"),
}

Classification of coroutines

Streams as a type of coroutine

We can classify everything seen in this presentation up until now:

YIELDSRESUMESRETURNS
ITERATORoption!!
FUTURE()wakerfuture output
STREAMfuture optionwaker!

Remark: GEN stands for Rust gen blocks. In general, in other languages, generators can also return values.

Table inspired by post by without.boats.

Synchronous vs. asynchronous things

Coroutines are part of a bigger classification of functions.

Synchronous functions:

TAKESCAPTURESYIELDSRESUMESRETURNS
Imperative loop {}!!!!!
Blocks {}!captured!!output
Function items fninput!!!output
Closures Fninputcaptured!!output
Iterator!!option!!
#[coroutine]input!itemanyany

Asynchronous functions:

TAKESCAPTURESYIELDSRESUMESRETURNS
Future!!()wakerfuture output
async {}!'static()wakerfuture output
Closures AsyncFninputcaptured!!future output
Stream!!future optionwaker!