Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

18. Async

Lesson 13 introduced computation expressions with result { } and seq { }. The third built-in builder is async { }, and it maps onto Python’s own async/await the way seq maps onto generators. An async { } block builds an Async a value. Inside it, let! awaits another async step, and return hands back the final value.

extern runAsync: Async a -> a = asyncio.run

let fetchScore =
  async {
    let! x = async { return 20 }
    return x + 1
  }

print (runAsync fetchScore)
21

The key idea carries straight over from Python. An async { } block does not run when you write it. It builds a coroutine, exactly like calling an async def in Python hands you an awaitable rather than a result. Nothing happens until something drives that coroutine. Here let! x = async { return 20 } awaits an inner block and binds 20 to x, then return x + 1 produces the final 21, but only once the coroutine is run.

The emitted Python shows the mapping directly. Each async { } becomes a nested async def, each let! becomes an await, and the whole value is a coroutine object:

import asyncio
async def _pf_fn1():
    async def _pf_fn0():
        return 20
    x = await _pf_fn0()
    return x + 1
fetchScore = _pf_fn1()
print(asyncio.run(fetchScore))

Running one at the top level

A coroutine has to be awaited somewhere, and at the top level there is no enclosing async function to await it in. Python solves this with asyncio.run(main()), and Pyfun makes the same move through extern (lesson 12). The line

extern runAsync: Async a -> a = asyncio.run

names Python’s asyncio.run and gives it the Pyfun type Async a -> a: hand it an Async a and it drives the coroutine to completion and returns the a. So fetchScore |> runAsync turns the Async int into a plain int you can print.

Async is an inferred effect

Lesson 11 showed the compiler inferring effects and checking let pure assertions. async is one of those effect labels. Effects are still inferred everywhere, and an extern arrow may state the async label explicitly with ->{async}, overriding the default io:

extern fetchAsync: string ->{async} string = httpx.get

A caller of fetchAsync then performs async, and that propagates outward. So a let pure body that performs async is a compile error, the same way a let pure that prints is:

extern fetchAsync: string ->{async} string = httpx.get

let pure grab url = fetchAsync url

print (grab "http://example.com")
error: `grab` is declared `pure` but performs `async`
 --> 3:21
  |
3 | let pure grab url = fetchAsync url
  |                     ^^^^^^^^^^^^^^

1 error

The effect is impossible to lie about. If a function awaits real async work, its type says so, and purity cannot be claimed over it.

Where async pays off, and where it does not

Async earns its keep when real I/O overlaps, so waiting on one network request or file read lets another proceed. The examples in this lesson are compute-shaped on purpose, because the browser playground has no network access, and Pyodide runs asyncio.run in its worker, so a self-contained block like fetchScore runs there and prints 21.

For real overlapping I/O, install the compiler with pip install pyfun-lang and read the http_fetch entry in the interop cookbook, which fetches URLs with inferred io and async effects over urllib and httpx. If you know Python’s asyncio, you can reach as far as you like through extern: any async client wraps the same way, and the effect system tracks it for you.

Exercise

Fill the hole so combined awaits two async blocks and returns their sum. The hole has type int (the value the second block returns), so any integer literal works. The runner bridge over asyncio.run is already in place, and runAsync combined drives the coroutine to a value.

extern runAsync: Async a -> a = asyncio.run

let combined =
  async {
    let! a = async { return 10 }
    let! b = async { return ? }
    return a + b
  }

print (runAsync combined)

Expected output:

30

Open in the playground

Show solution
extern runAsync: Async a -> a = asyncio.run

let combined =
  async {
    let! a = async { return 10 }
    let! b = async { return 20 }
    return a + b
  }

print (runAsync combined)

let! awaits each inner block and binds its result, so a is 10 and b is 20, and return a + b produces 30 once runAsync drives the coroutine.