Coroutines, asynchronous and iterative
I wanted to follow up my previous post with a small note elaborating on the use of
coroutines for asynchrony and iteration from a more abstract perspective. I realized the point I
made about AsyncIterator
being the product of Iterator
and Future
makes a bit more sense if
you also consider the “base case” - a block of code that is neither asynchronous nor iterative.
It’s also an excuse to draw another fun ASCII diagram, and I’ve got to put that Berkeley Mono license to good use.
By the “base case,” I mean just a normal block. This isn’t really a “coroutine” in the way the others are, of course, because its evaluated immediately in Rust, but you could imagine a language with lazy blocks that aren’t eagerly evaluated. I don’t want to get too in-the-weeds about laziness, I already know someone’s going to make some shallow comment on Reddit about Haskell and monads and how Rust’s design is fatally flawed, an opinion they are certainly free to hold. But if you ignore the distinction between lazy and eager semantics, you can imagine an ordinary block as also a kind of coroutine - one which is always ready (not asynchronous) and only evaluates once (not iterative).
What you get then is a diagram with two axes: asynchrony and iteration, and you can see how starting
from the base case, you can move toward AsyncIterator
by either path:
A S Y N C H R O N O U S
──────────────────────── ▶
╔═══════════════╗ ╔═══════════════╗
║ ║░░ ║ ║░░
║ BASE CASE ║░░ ║ FUTURE ║░░
║ ║───────────── ▶ ║░░
│ ║ { } ║░░ ║ async { } ║░░ │
I │ ║ ║░░ ║ ║░░ │ I
T │ ╚═══════════════╝░░ ╚═══════════════╝░░ │ T
E │ ░░░░░░│░░░░░░░░░░ ░░░░░░│░░░░░░░░░░ │ E
R │ │ │ │ R
A │ │ │ │ A
T │ │ │ │ T
I │ │ │ │ I
V │ ╔═══════▼═══════╗ ╔═══════▼═══════╗ │ V
E │ ║ ║░░ ║ ║░░ │ E
▼ ║ ITERATOR ║░░ ║ ASYNCITERATOR ║░░ ▼
║ ║───────────── ▶ ║░░
║ gen { } ║░░ ║ async gen { } ║░░
║ ║░░ ║ ║░░
╚═══════════════╝░░ ╚═══════════════╝░░
░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░
──────────────────────── ▶
A S Y N C H R O N O U S
This dynamic is also born out by adding the base case to the typing table from my previous post:
│ YIELDS │ RETURNS │ RESUMES
──────────────┼─────────────────────┼─────────────────┼─────────────────
│ │ │
BASE CASE │ ! │ Self::Output │ ()
│ │ │
FUTURE │ () │ Self::Output │ &mut Context
│ │ │
ITERATOR │ Self::Item │ () │ ()
│ │ │
ASYNCITERATOR │ Poll<Self::Item> │ () │ &mut Context
│ │ │
You can see how the baser case and Future
have the same return type, while the base case and
Iterator
have the same resume type; AsyncIterator
combines both Iterator
and Future
. And the
yield type actually reveals of the same progression on further consideration: the base case is !
,
an uninhabited type (a normal block never yields), whereas Future
yields the unit type to
represent Pending
, and Iterator
yields the item type: the AsyncIterator
then yields the sum of
Future
and Iterator
: a type that is either the item type or pending.