10.2. Example: encoding async/await
In Python and JavaScript, concurrent programs are commonly written with generators, which
are essentially coroutines. The following asyncio program fetches pages one round-trip at
a time:
import asyncio
async def fetch_pages(num_pages): # fetch every page: one round-trip per page
results = []
for page_num in range(num_pages):
await asyncio.sleep(0.1) # network I/O for this page
results.extend([page_num * 10 + i for i in range(2)])
return results
async def download_all(num_pages):
items = await fetch_pages(num_pages) # suspend on each of fetch_pages's awaits
return items
print(asyncio.run(download_all(3))) # [0, 1, 10, 11, 20, 21]
The same program written with plain generators makes the suspend/resume mechanism explicit:
await becomes yield, await coro becomes yield from, and asyncio.run becomes a
driver loop that repeatedly advances the coroutine (coro.send(None), i.e. next(coro))
until it raises StopIteration:
def fetch_pages(num_pages):
results = []
for page_num in range(num_pages):
yield # models `await asyncio.sleep(0.1)`
results.extend([page_num * 10 + i for i in range(2)])
return results
def download_all(num_pages):
items = yield from fetch_pages(num_pages) # `await fetch_pages(...)`: delegate,
return items # re-yielding each suspension
def run(coro):
try:
while True:
coro.send(None) # the driver's resume: advance one suspension
except StopIteration as done:
return done.value
print(run(download_all(3))) # [0, 1, 10, 11, 20, 21]
The Laurel encoding below mirrors this generator form: yield for await asyncio.sleep,
return for the final result, a driver loop in run for the trampoline, and inside
download_all the loop that yield from fetch_pages(num_pages) compiles to. Two of the
ingredients are not implemented yet — a coroutine return value and the await /
yield from sugar itself — and both depend on the Future channel described in
Planned: Future channels for await; the encoding
spells them out manually. One representational simplification keeps it small: items are
summed into a running int rather than collected into a list (Laurel has no growable list
yet).
coroutine fetch_pages(num_pages: int): int
{
var results: int := 0;
var page_num: int := 0;
while (page_num < num_pages) { // Python: `for page_num in range(num_pages)`
yield; // models `await asyncio.sleep(0.1)`
// This page's items, summed: `[page_num*10, page_num*10+1]`.
results := results + page_num * 10 + (page_num * 10 + 1);
page_num := page_num + 1
};
return results // models `return results`
};
coroutine download_all(num_pages: int): int
{
// `await fetch_pages(num_pages)` / `yield from fetch_pages(num_pages)`, expanded to
// the loop it compiles to (see below):
var co: fetch_pages := fetch_pages(num_pages); // spawn the delegate
while (has_next(co)) { // while it has more steps to run
resume(co); // advance it one step (Pending or Done)
if has_next(co) then { // it suspended (Pending), not finished →
status := Pending(); yield // re-surface as download_all's suspension
}
};
return done_value(co) // delegate done: read its Done payload
};
procedure run(): int // Python's trampoline (`run`): drive to completion
{
var co: download_all := download_all(3);
while (has_next(co)) { // Python: `while True: coro.send(None)`
resume(co) // the driver's one-step advance
};
return done_value(co) // Python: the `StopIteration.value` (co's Done payload)
};
Two operations at opposite ends of a control transfer appear here. resume is the
driver side: run advances a coroutine by one suspension, mirroring coro.send(None)
(≡ next(coro)). await / yield from is the delegate side. It is not a primitive
but the while (has_next(co)) { … } loop shown, which a single
var items := await fetch_pages(num_pages) would compile to. It drives fetch_pages
step by step and, since download_all itself returns a value (channel Future<int>),
re-surfaces each delegate suspension as its own Pending (status := Pending(); yield).
That is why download_all, though it delegates internally, is still advanced only by an
outer resume from run.
The same has_next serves as both loop guard and inner test: before a step, "more to
do?"; after it, "suspended or finished?". On finish, done_value(co) reads the delegate's
Done payload — the value stored at completion — so the result is recovered even if some
other resume drove the delegate to completion while download_all was suspended.