Skip to content
📖 Auto vs RustBook contents

Async Programming with ~T ​

Many operations we ask a computer to do could take a while to finish. In Auto, as in most languages, these kinds of operations are called asynchronous or async. The ~T type modifier is Auto's way of expressing a value that represents a computation which will complete at some point in the future — a time blueprint.

In this chapter, we'll cover:

  • What ~T means and how it relates to Rust's Future trait
  • How to create and compose async operations
  • Concurrency patterns with async tasks
  • Streams: async iterators over time
  • How ~T interacts with Auto's actor model
  • When to use actors vs async vs threads

What Is a Time Blueprint? ​

Consider two different operations:

  1. Rendering a video — this is CPU-bound. Every frame must be computed, and more CPU power directly means faster rendering. This is parallelism: doing multiple things at the same time.

  2. Downloading a video — this is I/O-bound. The network determines the speed, not your CPU. While waiting for data, your CPU sits idle. This is where concurrency shines: you can start a download, switch to other work while waiting, and come back when data arrives.

Parallelism vs Concurrency: Parallelism is about doing multiple things simultaneously. Concurrency is about managing multiple tasks by switching between them. You can have concurrency on a single CPU core. Parallelism requires multiple cores.

Rust's Future trait represents a value that may not be ready yet. Auto's ~T takes this concept further: ~T is a time blueprint — a description of a value that will materialize in the future. The ~ symbol represents the temporal dimension: the value exists, just not yet.

  • int — a value you have right now
  • ~int — a blueprint for an int that will arrive later
  • ~String — a blueprint for a String that will arrive later

Note: In Rust, async functions return impl Future<Output = T>. In Auto, async functions return ~T. The blueprint metaphor is intentional: ~T describes how to produce the value, not the value itself. Like an architectural blueprint, it must be "built" (awaited) to produce the result.

Creating Async Functions ​

In Auto, an async function is one that returns a ~T type. You use the async keyword to mark a function as asynchronous, and .await to wait for a blueprint's result.

Your First Async Function ​

An async function that fetches a page title

There are several important things to notice here:

  1. page_title returns ~?String — a blueprint for an optional string
  2. The .await keyword suspends execution until the blueprint resolves
  3. async.block sets up the async runtime and waits for the result
  4. The main function itself is not async — it enters the async world via async.block

The async.block Entry Point ​

Unlike Rust, where you need an external runtime like tokio, Auto has a built-in async runtime. async.block is the bridge from synchronous to asynchronous code:

auto
fn main() {
    async.block(async {
        // You can use .await here
    })
}
rust
fn main() {
    trpl::block_on(async {
        // You can use .await here
    })
}

async.block takes a blueprint and materializes it — it runs the async runtime, executes the blueprint, and returns the final value. This is the entry point for all async code in an Auto program.

What Happens at .await ​

When you call .await, the following happens:

  1. If the value is ready, return it immediately
  2. If not, suspend the current computation and yield control to the runtime
  3. The runtime can switch to other pending blueprints
  4. When the value becomes available, resume the computation

This is cooperative multitasking: blueprints yield control at await points, and the runtime schedules them fairly. Unlike OS threads, which are preemptively scheduled, async tasks choose when to yield.

Key Insight: In Rust, this suspension and resumption is implemented via a state machine that the compiler generates for each async block. Auto uses the same technique internally, but exposes it through the cleaner ~T type notation.

Concurrency with Async ​

The real power of async becomes apparent when you have multiple operations that can proceed concurrently.

Racing Two Operations ​

Racing two page title fetches

async.race starts both blueprints and returns as soon as either one completes. The other blueprint is cancelled. This is useful when you want the fastest response and don't need both results.

Spawning Async Tasks ​

Sometimes you want to run a blueprint in the background without waiting for it immediately. Auto provides async.spawn for this:

Spawning async tasks

async.spawn starts a blueprint as a separate task and returns a handle. The handle is itself a ~T — a blueprint for the task's result. You can continue doing other work and .await the handle when you need the result.

Joining Multiple Blueprints ​

While race returns the first result, join waits for all blueprints:

Joining two async tasks

async.join runs both blueprints concurrently and returns both results as a tuple. If either blueprint panics, the other is cancelled.

Async Channels ​

Just as actors communicate via message passing, async tasks can communicate through async channels. An async channel is like a pipe: one end sends values, the other end receives them asynchronously.

Async channel message passing

The channel has two ends:

  • tx (transmitter): sends values with tx.send(value).await
  • rx (receiver): receives values with rx.recv().await

The rx.recv() returns ~?T — a blueprint for an optional value. It returns Some(value) when a message arrives, and None when the channel is closed (i.e., all transmitters have been dropped).

Multiple Producers ​

You can have multiple tasks sending to the same channel by cloning the transmitter:

Multiple producers on one channel

async.join_all takes a list of blueprints and waits for all of them to complete. This is the multi-blueprint counterpart to async.join.

Yielding Control ​

Async tasks cooperate by yielding control at .await points. But what if a task does heavy computation without any await points? It would monopolize the runtime, starving other tasks.

The Problem: Blocking Without Awaiting ​

A slow computation blocking the runtime

Because slow has no .await points, task A runs to completion before task B even starts. There is no interleaving — no concurrency.

The Solution: async.yield_now ​

Yielding control with `async.yield_now`

async.yield_now() explicitly yields control back to the runtime, allowing other tasks to make progress. Use it in CPU-heavy loops that don't have natural await points.

Rule of Thumb: If you have a long-running computation inside an async context, periodically call async.yield_now() to be a good citizen of the cooperative runtime.

Composing Blueprints: Timeout ​

One of the most powerful patterns in async programming is composing blueprints to build higher-level abstractions. Let's build a timeout function that wraps any blueprint with a time limit.

Building a timeout from `race` and `sleep`

The timeout function races the given blueprint against async.sleep. If the blueprint finishes first, we return its value. If the sleep finishes first, we return an error. This is a clean example of blueprint composition: complex behavior built from simple primitives.

Streams: Async Iterators ​

A ~T blueprint produces a single value. But many real-world data sources produce multiple values over time: WebSocket messages, file lines, sensor readings, UI events. For these, Auto provides streams.

A stream is to ~T what an iterator is to a regular value: it produces a sequence of values asynchronously.

Creating and Consuming Streams ​

Creating a stream from an iterator

The while let Some(value) = stream.next().await pattern is the async equivalent of for value in iterator. Each call to stream.next().await produces the next value, suspending if none is available yet.

Stream from a Channel ​

Channels are naturally stream-like — values arrive over time:

Using a channel receiver as a stream

Composing Streams ​

Like iterators, streams support combinators for transforming and filtering:

Stream combinators

Actors and Async: When to Use Which ​

Auto provides two primary concurrency models: actors (Chapter 16) and async (this chapter). Both are built into the language and runtime. Here's how to decide which to use:

ScenarioUse ActorsUse Async (~T)
Independent services with stateYesNo
Request/response workflowsEitherYes
Streaming dataNoYes
State machinesYesEither
CPU-bound parallelismLimitedUse threads
Many I/O-bound operationsEitherYes
Message-based communicationYesChannels

Combining Actors and Async ​

Actors and async can be combined. An actor can .await blueprints inside its message handlers:

An actor that uses async operations

In this pattern, the actor's on handler is itself async — it returns ~T. The ask call returns a blueprint that resolves when the actor finishes processing the message.

Threads, Actors, and Async ​

Auto supports three levels of concurrency, each suited to different problems:

  1. OS Threads — for CPU-bound parallelism (via use.rust)
  2. Actors — for stateful, message-passing concurrent services
  3. Async Tasks — for I/O-bound operations and streaming data

The async runtime multiplexes many lightweight tasks onto a pool of OS threads, using work stealing to balance load. This means you can have thousands of concurrent async tasks without thousands of OS threads.

Under the Hood: How ~T Works ​

Understanding what happens inside ~T helps you write better async code.

Blueprints as State Machines ​

When the Auto compiler encounters an async function, it transforms it into a state machine. Each .await point is a state transition:

auto
async fn page_title(url String) ~?String {
    // State 1: Initial
    let response = http.get(url).await        // -> State 2
    // State 2: Got response
    let text = response.text().await           // -> State 3
    // State 3: Got text
    Html.parse(text).select_first("title")
        .map((t) => t.inner_html())
}

Conceptually, the compiler creates something like:

auto
enum PageTitleBlueprint {
    State1 { url String },
    State2 { response HttpResponse },
    State3 { text String },
    Done,
}

Each time poll is called on the blueprint, it advances to the next state or returns Pending if it needs to wait.

Pinning ​

One subtlety of async state machines is that they can be self-referential: one state might hold a reference to data from a previous state. If the blueprint were moved in memory, those references would become invalid.

Auto handles this automatically: blueprints are pinned in memory once they begin executing. You don't need to think about pinning in normal code, but it's worth knowing it exists under the hood.

In Rust, this is expressed with the Pin<P> type and the Unpin marker trait. Auto's runtime manages pinning implicitly, so you never need to annotate lifetimes or use Pin<Box<...>>.

The ~T Type Hierarchy ​

TypeMeaningExample
TA value available now42, "hello"
~TA blueprint for a future Thttp.get(url)
~?TA blueprint for a future optional Tpage_title(url)
~!TA blueprint for a future result Ttimeout(task, 1000)
Stream<T>A sequence of ~?T values over timerx.stream()

The modifiers compose naturally: ~?T is a blueprint for an optional value, ~!T is a blueprint that may fail.

Async Quick Reference ​

FeatureAuto (~T)Rust (Future)
Return type~Timpl Future<Output = T>
Wait for result.await.await
Enter async worldasync.blockblock_on / #[tokio::main]
Spawn taskasync.spawntokio::spawn / trpl::spawn_task
Race first resultasync.race(a, b)tokio::select! / trpl::select
Wait for allasync.join(a, b)tokio::join! / trpl::join
Wait for manyasync.join_all(list)tokio::join_all / futures::join_all
Sleepasync.sleep(ms)tokio::time::sleep / trpl::sleep
Yield controlasync.yield_now()tokio::task::yield_now()
Channelasync.channel(T)tokio::sync::mpsc / trpl::channel
Timeoutasync.race + sleeptokio::time::timeout
Async sequenceStream<T>impl Stream<Item = T>
RuntimeBuilt-inExternal (tokio, async-std)

Summary ​

Auto's ~T async model provides a clean, composable way to handle asynchronous operations:

  1. ~T — a time blueprint representing a future value
  2. .await — materialize a blueprint, suspending until it resolves
  3. async.block — enter the async world from synchronous code
  4. async.spawn — run a blueprint as a separate task
  5. async.race — compete blueprints, first wins
  6. async.join — run blueprints concurrently, wait for all
  7. async.channel — communicate between async tasks
  8. async.yield_now — cooperatively yield control
  9. Streams — async iterators for sequences of values over time
  10. Composition — build complex behavior from simple blueprints

The ~T system integrates naturally with Auto's actor model: actors can await blueprints, and async tasks can send messages to actors. Together, they provide a complete concurrency story that covers CPU-bound work (threads), stateful services (actors), and I/O-bound operations (async).

In the next chapter, we'll explore Auto's approach to object-oriented programming with the is, has, and spec keywords.

Released under the MIT License.