Skip to content
📖 The Auto Programming LanguageBook contents

Async with ~T ​

In Chapter 15, we learned about Auto's actor model -- how to spawn tasks, send messages, and receive with on. Actors give us concurrency through message passing. But there is a second concurrency model in Auto that is just as important: async programming with the ~T type modifier.

In this chapter, we will learn how Auto's ~T (pronounced "tilde-T") provides a clean, composable way to express asynchronous operations -- without the ceremony of async/await keywords or external runtimes.

Async in Auto ​

Most programming languages handle asynchronous operations with two keywords: async to mark a function, and await to wait for its result. JavaScript has async/await. Python has async/await. C# has async/await. Rust has async fn and .await.

Auto takes a different approach. Instead of adding keywords, Auto uses a type modifier: the tilde ~. When you see ~String, it means "a String that will arrive in the future." When you see ~int, it means "an int that will arrive in the future." The ~ is a time blueprint -- a description of a computation that will complete at some point.

Why not async/await? Three reasons:

  1. Keywords are heavy -- async/await requires two new keywords in the language. ~T requires zero new keywords. The tilde is a type modifier, like ?T for optional or !T for result.

  2. Composition is natural -- ~T composes with other type modifiers. You can write ~?String (a future optional string) or ~!int (a future result int). With async/await, these compositions are awkward or impossible.

  3. Actors and async unify -- In Auto, on already handles waiting for a message. The ~T type lets async functions participate naturally in the actor system without a separate runtime or a different set of rules.

This is the same ~T concept that the Rust companion book explores in depth in its Chapter 17. Here in TAPL, we focus on how ~T works within Auto's actor-based concurrency model.

~T Blueprints ​

A function that returns ~T is an async function. It does not return the value immediately -- it returns a blueprint for a value that will be produced later. The caller can decide when to wait for it.

A function returning a time blueprint

Let us break down the key ideas:

  1. fn fetch_data() ~String -- The return type is ~String, not String. This tells the compiler (and the reader) that the function returns a time blueprint. The actual string will be produced later.

  2. on result String -- Inside the async function, on waits for a trigger. When the function is called, the caller receives a blueprint immediately. The function body runs asynchronously, and when it reaches on, it waits for the condition that will produce the result.

  3. send main result -- When the result is ready, it is sent back to the caller (in this case, main). This completes the blueprint.

  4. spawn fetcher -- The fetch_data function is spawned as an actor. Because it returns ~String, the spawn creates an async actor that will produce a string at some point.

  5. on data String -- The caller uses on to receive the completed value. This is the same on keyword we learned in Chapter 15, but here it is receiving the result of an async operation.

The key insight is that ~T and the actor model use the same primitives: on to wait, send to deliver. There is no separate async runtime. Async functions are actors that happen to return a value.

In TypeScript, the equivalent is Promise<string> -- a promise that resolves to a string. In Rust, the equivalent is impl Future<Output = String>. Auto's ~String is shorter, clearer, and composes with the type modifier system.

Async Functions ​

Now let us see a more complete async function -- one that takes parameters and produces a result.

An async function with parameters

This prints:

Hello, Alice!

Several things to notice:

  1. fn delayed_greeting(name String) ~String -- The function takes a String parameter and returns ~String. The ~String return type tells Auto this is an async function. It will produce a string, but not right now.

  2. on _ -- The underscore _ means "wait for any trigger." In a real async function, this would be a network response, a timer, or a message from another actor. Here it simulates waiting for async work to complete.

  3. send caller -- The special identifier caller refers to whoever called this async function. This is the same caller we saw with actors in Chapter 15, but in the async context it means "the place that holds the blueprint."

  4. var result = delayed_greeting("Alice") -- The call returns immediately with a blueprint. The variable result holds a ~String, not a String. No actual greeting has been produced yet.

  5. on msg String -- The main actor waits for the blueprint to complete. When the greeting is ready, msg receives the actual String value.

The flow is: call returns a blueprint -> async function runs in background -> result is sent back -> caller receives it with on. This is the same pattern as actor message passing, but the type system tracks the async contract.

In Rust, this would require async fn and .await. In Python, async def and await. In TypeScript, Promise and .then(). In Auto, it is just ~T and on.

on for Async Handlers ​

The on keyword serves double duty in Auto. In Chapter 15, we used it to receive actor messages. In async functions, we use it to wait for a result. This is not a coincidence -- it is the same mechanism. Async functions are actors.

Using `on` to handle async request/response

This prints:

Status: 200

This example shows the natural blend of actors and async:

  1. fn http_server() -- A regular actor function with a message loop (for true with on). It is not declared as async -- it is an actor that happens to process requests and send responses.

  2. type Request and type Response -- Custom types (from Chapter 6) that define the protocol between the caller and the server. The server receives Request messages and sends back Response messages.

  3. send http_server Request { url: "/api/data" } -- The caller sends a request to the server actor. This is fire-and-forget, just like any actor message.

  4. on resp Response -- The caller waits for a Response to arrive. This is the async part: the caller suspends until the server sends back a response.

The http_server function is a long-running actor. It is not itself async in the ~T sense -- it does not return a blueprint. Instead, it is a service that processes requests and sends responses. The caller interacts with it using the async pattern: send to make a request, on to wait for the response.

In Rust, this pattern requires explicit channels, a loop, and recv(). In Python, threading.Thread and Queue. In C, there is no standard equivalent. In Auto, it is the same on/send pattern we use for all actor communication.

Composing Async ​

The real power of ~T appears when you need to chain multiple async operations. When one async operation depends on the result of another, you nest on blocks. Each on waits for its blueprint to complete before the inner code runs.

Chaining async operations

This prints:

User: User1
Posts: Posts for User1

This is the async chaining pattern:

  1. var user = fetch_user(1) -- Call the first async function. user is a ~String blueprint. The function starts running in the background.

  2. on u String { ... } -- Wait for the first blueprint to complete. When it does, u holds the actual String value ("User1").

  3. var posts = fetch_posts(u) -- Use the result of the first operation as input to the second. This only runs after the first on completes, so u is guaranteed to be a real value.

  4. on p String { ... } -- Wait for the second blueprint to complete. Inside this block, both u and p are available as real values.

The nesting of on blocks is the Auto equivalent of chaining .then() in JavaScript or sequential .await in Rust. Each on introduces a scope where the blueprint has been resolved, and the value is available for use.

In TypeScript, this would be await fetchUser(1) followed by await fetchPosts(u). In Rust, it would be let u = fetch_user(1).await followed by let p = fetch_posts(&u).await. In Auto, the on blocks achieve the same thing while staying within the actor message-passing paradigm.

Async + Actors ​

The deepest insight about Auto's async model is that async and actors are the same thing. An async function is an actor that returns a value. An actor is a function that runs concurrently. The ~T type modifier is what bridges them: it marks a function as one that will produce a result for its caller.

This means actors can call async functions, async functions can send messages to actors, and everything composes naturally.

Combining async functions with the actor model

This prints something like:

Processed: task-a
Processed: task-b

(The order may vary since the workers run concurrently.)

This example brings together everything we have learned:

  1. enum Work -- Defines the message protocol. Process(String) is a command to do work; Result(String) is the completed result. This is the same pattern from Chapter 15 for actor communication, and from Chapter 9 for error handling.

  2. fn worker(id int) -- A parameterized actor (from Chapter 15). Each instance has its own id and its own mailbox. Workers run as independent actors.

  3. is work { Process(data) => ..., Result(_) => ... } -- Pattern matching on the Work enum (from Chapter 7). The worker processes Process messages and ignores Result messages.

  4. spawn worker(1), spawn worker(2) -- Two independent worker actors. Each processes messages from its own mailbox.

  5. send worker(1) Work.Process("task-a") -- Dispatch work to specific workers. The Work enum wraps the data with type information.

  6. on result Work { is result { Result(msg) => print(msg), ... } } -- The main actor receives results from the workers and pattern matches to extract the message.

The Work enum is the key design pattern here. By wrapping both commands and results in the same enum type, the actor system provides type-safe communication. The compiler ensures that every variant is handled, preventing bugs from unhandled message types.

This is the actor-async pattern that makes Auto powerful as an AI Operating System. Workers are actors. Tasks are messages. Results are messages. The type system ensures everything fits together. No shared memory, no locks, no race conditions.

Timeout and Cancellation ​

When you spawn an async operation, you may not want to wait forever. What if the network is slow? What if the computation takes too long? You need a way to set a time limit and cancel the operation if it does not complete in time.

A long-running async operation with potential timeout

This prints:

Done!

This example shows the basic structure for timeout and cancellation. The long_running function simulates an operation that takes time. The main function starts it and waits for the result with on.

In a real implementation, you would add a timeout mechanism. The pattern would be:

  1. Start the async operation (var job = long_running())
  2. Start a timer concurrently
  3. Use on to receive whichever completes first -- the result or the timeout

Because Auto's actor model is message-based, cancellation is also message-based. You would send a cancellation message to the actor running the long operation, and the actor would check for it in its message loop. This is fundamentally simpler than thread cancellation in other languages, where you have to deal with interrupt flags, CancellationToken, or thread.interrupt().

The key insight is that in Auto, everything is an actor, and actors communicate through messages. This means timeout and cancellation are not special runtime features -- they are just another message pattern. You do not need a special API. You do not need a CancellationToken type. You send a message that says "stop" and the actor stops.

In Rust, the equivalent pattern uses tokio::select! or tokio::time::timeout. In Python, asyncio.wait_for. In JavaScript, Promise.race. In Auto, the same actor primitives handle both normal completion and timeout.

Summary ​

This chapter introduced ~T -- Auto's approach to async programming. Here is what we learned:

ConceptSyntaxPurpose
Async return typefn foo() ~TDeclares a function that returns a future value
Wait for resulton var T { }Blocks until the blueprint resolves
Reply to callersend caller valueCompletes the blueprint with a value
Chain async opsNested on blocksRun sequential async operations
Compose with actorsspawn + send + onMix async functions and actor patterns
Timeout/cancelMessage-basedSend cancellation as a regular message

Key takeaways:

  1. ~T is a type modifier, not a keyword -- It uses the same system as ?T for optional and !T for result. No new keywords needed.

  2. Async and actors are the same -- An async function is an actor that returns a value. The on/send primitives work for both.

  3. Composition is through nesting -- Chain async operations by nesting on blocks. Each on resolves a blueprint and makes the value available.

  4. Type safety throughout -- The compiler tracks which values are blueprints (~T) and which are resolved (T). You cannot accidentally use a blueprint as a real value.

  5. Timeout is just a message -- Because everything is actor-based, timeout and cancellation are message patterns, not special runtime features.

The ~T type modifier is the final piece of Auto's concurrency story. Combined with the actor model from Chapter 15, it provides a complete system for writing concurrent programs: actors for long-running services with state, and ~T for request/response patterns that produce a result. Together, they make Auto a natural fit for the AI Operating System model, where dozens of services run concurrently, communicate through messages, and compose their results.

In the next chapter, we will explore how Auto handles object-oriented programming with the is, has, and spec keywords.

Released under the MIT License.