Role of coroutines

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

Previous post: Making generators.

In some other posts on this site, you will find ways to create streams from scratch and how to combine them. This post will be about the relationship between the concept of a Stream (or asynchronous iterator) and the other, more familiar, functions present in most programming languages.

Most of this post was inspired by a post by without.boats.

Simple coroutines

Concept of a coroutine

Normal functions return output (immediately). They do it only once (or never).

A coroutine is a special kind functions that can:

More specifically, at runtime, coroutines go through a process (with specific terminology):

  1. When a coroutine suspends, it yields a value to the caller. This is a kind of intermediate return value.
  2. After observing (or ignoring) the yielded value, the caller can safely forget about the suspended coroutine (temporarily) and continue with other functions.
  3. Later the caller can return to this suspended coroutine. The caller needs to resume the suspended coroutine to wake it up. This step is called resumption. For resumption some resumption data may need to be provided.

These steps may repeat forever or until the coroutine ends by returning. Returning is distinct from yielding, since it is final. The return value is the last value that can be observed by the caller.

Remark: Coroutines are used internally by the Rust compiler while compiling asynchronous code. The compiler implements a form of “stack-less” co-routines for async {} code-blocks. These blocks are compiled implicitly into coroutines that yield at every await-point.

Directly constructing coroutines

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

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

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

Notice that we need Pin, similarly to Future. This is because coroutines may be self-referential. The resume function should only be called on coroutines that may move (are Unpin). The reason is probably that they should extend the behaviour of Futures which do require Pin.

Important: The Coroutine trait in Rust is unstable and only available on nightly as of April 2025.

If you look carefully at the Coroutine trait you could see that (in pseudo-code):

type Future<Output> = Coroutine<
    Resume = Context, 
    Yield = (), 
    Return = Output
>;

More precisely, a future is a coroutine that yields nothing when suspended. A future needs a Context (containing a Waker) to be resumed or woken.

Remark: The resumption data is provided by a asynchronous run-time to schedule resumes in an efficient way.

Example of a coroutine

The Rust docs contain an example of a coroutine. The coroutine does not need any resumption data, but it yields a number and returns a string on completion:

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

To use this coroutine, we have to provide an initial chunk of resumption data. By default this is the empty tuple (). The resumption data is passed to the resume function and used to anticipate the first yield. The first (and last) yield is yield 1.

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

The next time resume is called, no yield is encountered and the final return value is returned (a string).

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

If our coroutine was a Future, then resume would expect a Context with a Waker.

Classification of coroutines

Reflecting on the concepts of an iterator, future and stream, we can say that:

Coroutines are a generalisation of these cases, which can be layed-out in a table:

YIELDSRESUMESRETURNS
IteratorOption!!
Future, AsyncFn()WakerAny
StreamOptionWaker!
CoroutineAnyAnyAny

In this table, the ! symbol stands for never, the type that does not have any runtime value. In other words, never is not constructible. It is used often as the return time of non-terminating functions like infinite loops.

For a practical introduction to coroutines in Rust, I recommend Asynchronous Programming in Rust.