Async And Generators

Async Functions

Async functions use async func. Calling one starts it and returns a Task. The call itself does not block; the program only waits when it reaches await or task.await().

async func double(int x): int {
    return x * 2;
}

let task = double(21);
io.println("calculation has started");
io.println(await task);

Tasks can also be inspected:

io.println(task.done());
io.println(task.await());

Async Module

import async;

let worker = async.run(func(): int {
    return 42;
});

io.println(async.await(worker));
await async.sleep(100);

async.done(task) reports completion.

Combining tasks

async.all([tasks]) returns a new task that completes when every input task has completed. The result is a list of values in the original order. If any input fails, the returned task fails immediately with the first error and the remaining tasks are cancelled.

import async;
import io;

let pageA = async.run(func(): string {
    await async.sleep(40);
    return "A";
});
let pageB = async.run(func(): string {
    await async.sleep(60);
    return "B";
});

let pages = await async.all([pageA, pageB]);
io.println(pages[0]);  # "A"
io.println(pages[1]);  # "B"

async.race([tasks]) returns a task that completes with the first finisher's value. The rest are cancelled.

let fast = async.run(func(): string {
    await async.sleep(20);
    return "fast";
});
let slow = async.run(func(): string {
    await async.sleep(200);
    return "slow";
});

io.println(await async.race([fast, slow]));  # "fast"

Timeouts

async.timeout(task, ms) wraps a task with a deadline. If the task does not complete within the given milliseconds, the wrapping task fails with a RuntimeError and the inner task is cancelled.

let slow = async.run(func(): string {
    await async.sleep(500);
    return "never";
});

# Awaiting the timeout wrapper raises a runtime error when the deadline expires:
let result = await async.timeout(slow, 50);   # runtime error after 50ms

Cancellation

A task can be cancelled explicitly with async.cancel(task) or task.cancel(). Cancelling marks the task complete with a sentinel error, so any pending or later await returns with that error. Use task.cancelled to check the state non-destructively.

let job = async.run(func(): int {
    await async.sleep(1000);
    return 1;
});

job.cancel();
io.println(job.cancelled);   # true

Doing Work While A Task Runs

A common mistake is to start a task and immediately await it. That is valid, but it gives the program no opportunity to do useful work concurrently.

This version blocks straight away:

let result = await fetchReport();
io.println(result);

This version starts the work, continues with local work, then waits only when the result is actually needed:

async func fetchReport(): string {
    await async.sleep(100);
    return "report ready";
}

let reportTask = fetchReport();

io.println("started report");
io.println("loading local config");
io.println("rendering progress indicator");

let report = await reportTask;
io.println(report);

For polling-style workflows, use async.done:

import async;
import io;

async func background(): string {
    await async.sleep(150);
    return "background result";
}

let task = background();

while (!async.done(task)) {
    io.println("tick");
    await async.sleep(25);
}

io.println(async.await(task));

The important rule is that a task value is just a value. Store it in a variable, put it in a list, pass it to another function, or await it later.

func awaitAll(list<Task<string>> tasks): list<string> {
    list<string> results = [];
    for (task in tasks) {
        results.push(await task);
    }
    return results;
}

Concurrent I/O Shape

Async is most useful around I/O, timers, HTTP calls, sockets, and work that can run independently. Prefer the async stdlib wrappers for I/O instead of writing your own async func around synchronous calls:

import async.io as aio;

let configTask = aio.readText("config/app.json");
let templateTask = aio.readText("templates/page.html");

io.println("both reads have started");

let config = await configTask;
let template = await templateTask;

If the second operation depends on the first result, await between them:

let config = await readConfig();
let template = await readTemplateFor(config);

That sequential form is clearer and avoids pretending dependent work is parallel.

Async modules currently include:

  • async.io: file and handle reads/writes.
  • async.http: HTTP client calls.
  • async.net: TCP/UDP socket operations.
  • async.stream: JSON, YAML, XML, and CSV streaming parser work.

These APIs return Task values. Their current implementation runs host I/O work outside the caller's flow; the roadmap target is a shared event-loop scheduler that can power higher-throughput networking engines without changing these public module APIs.

Example socket workflow:

import async;
import async.net as anet;
import bytes;
import net;

let listener = await anet.listenTcp("127.0.0.1:0");
let address = net.localAddr(listener);

let server = async.run(func(): string {
    let conn = await anet.accept(listener);
    let message = await anet.read(conn, 4);
    await anet.write(conn, "pong");
    await anet.close(conn);
    return bytes.toString(message);
});

let client = await anet.connectTcp(address);
await anet.write(client, "ping");
io.println(bytes.toString(await anet.read(client, 4)));
io.println(await server);

Error Handling In Tasks

Errors thrown inside an async task are re-raised when the task is awaited:

async func loadRequired(string path): string {
    if (!io.exists(path)) {
        throw IOError("missing file: " + path);
    }
    return io.readText(path);
}

let task = loadRequired("settings.json");

try {
    io.println(await task);
} catch (IOError e) {
    io.println("could not load settings: " + e.message);
}

Use this pattern to start work early, then handle the failure at the point where the result becomes necessary.

Generators

Generator functions use yield and return lazy generator values:

func numbers(): generator<int> {
    yield 1;
    yield 2;
    yield 3;
}

for (n in numbers()) {
    io.println(n);
}

Function literals can be generators:

let upTo = func(int max): generator<int> {
    for (let int i = 1; i <= max; i++) {
        yield i;
    }
};

Iterable Parameters

Use iterable<T> when an API accepts generator-like inputs:

func sum(iterable<int> values): int {
    int total = 0;
    for (n in values) {
        total += n;
    }
    return total;
}

Generators are lazy. Breaking out of a for-in loop closes the producer so unbounded streams do not continue running.

func lines(list<string> paths): generator<string> {
    for (path in paths) {
        for (line in io.readText(path).split("\n")) {
            yield line;
        }
    }
}

for (line in lines(["a.log", "b.log"])) {
    if (line.contains("ERROR")) {
        io.println(line);
        break;
    }
}

Generators and async solve different problems. Generators avoid building a full collection before iteration. Async tasks allow other work to continue while a result is pending. For a large remote dataset, an API should usually combine both ideas: fetch or read chunks asynchronously, then yield records lazily.