Tzopilotl
Docs
GitHub

Tzopilotl by Example

25. Async and Await

An async fn is a function whose body may suspend at an await and resume later, delivering its single result through a Future<T>. Async functions are built directly on coroutines: an async fn compiles to a coroutine whose suspension points are awaits. Calling one runs its body eagerly up to the first await (or to completion) and hands back a Future.

25.1 Async Functions and await

Declare an async function with async fn. Inside it, await unwraps a Future<T> to its value T. The function's external return type is Future<T> even though its body produces a plain T.

async fn compute(x Int) Int {
    let a = await ready(x + 1);
    let b = await ready(a * 10);
    b + 5
}

-- compute returns Future<Int>; await it to get the Int
let r = await compute(4);
r println;   -- 55

At the top level (or any non-async context) await blocks until the future resolves; inside an async fn it suspends cooperatively and resumes when the awaited future is ready. It is the same keyword either way.

25.2 The Future Type and ready

ready(x) wraps a value in an already-resolved Future<T> — handy for returning a constant from an async context, or in tests.

let f = ready(42);        -- f : Future<Int>
let v = await f;          -- 42

-- An async fn that takes and returns values:
async fn greet(name String) String { "hi " $ name }

let g = greet("Ada");    -- runs eagerly; g : Future<String>
(await g) println;        -- hi Ada

25.3 Awaiting Time with delay

delay(beats) returns a Future<Void> that resolves after the given number of beats. Awaiting it suspends the async function until that time arrives, so a beat-based sequence reads top to bottom.

The beats are logical time. On a silo VM the audio clock drives them, so await delay there waits in real, sample-accurate time. On the main (NRT) VM a top-level await advances the timeline straight to the next timer, so the sequence above prints immediately in the right order without taking three beats of wall-clock time — which is also what lets offline renders run faster than real time. When code really must wait in wall-clock time (pacing a script against the outside world), use await delayReal(seconds) from the clock module; to await beats on the live tempo clock (tracking tempo changes and ramps like a sched callback), use await delayBeats(beats) (see the FFI Guide, §11.7).

async fn afterDelay(tag String, beats Float) Void {
    await delay(beats);
    tag println;
}

-- Start three out of order; they resolve in beat order.
let a = afterDelay("A(3)", 3.0);
let b = afterDelay("B(1)", 1.0);
let c = afterDelay("C(2)", 2.0);
await a; await b; await c;
-- B(1)  C(2)  A(3)

25.4 Combinators: awaitAll and gather

The futures module provides combinators over a list of futures. awaitAll is a barrier — it returns once every future has resolved. gather also collects their values, in argument order (independent of the order they resolved in).

import std.futures.*;

-- awaitAll: wait for all of them (a barrier)
await awaitAll([afterDelay("x", 1.0), afterDelay("y", 2.0)]);

async fn load(id Int, beats Float) Int {
    await delay(beats);
    id * 10
}

-- gather: results come back in argument order, not resolve order
let xs = await gather([load(1, 2.0), load(2, 1.0), load(3, 3.0)]);
xs println;   -- [10, 20, 30]

Because each load(...) runs eagerly to its first await before gather is called, the three loads are all in flight at once; gather just waits for them and assembles the results in order.

25.5 How It Works