Swift `async let` Bindings

Swift structured concurrency provides a paradigm for spawning concurrent child tasks in scoped task groups, establishing a well-defined hierarchy of tasks which allows for cancellation, error propagation, priority management, and other tricky details of concurrency management to be handled transparently.

Task groups are a very powerful, yet low-level, building block useful for creating powerful parallel computing patterns. They work best for spreading out computation of same-typed operations.

In that sense, task groups are a low level implementation primitive, and not the end-user API that developers are expected to interact with a lot, rather, it is expected that more powerful primitives are built on top of task groups.

While task groups are indeed very powerful, they are hard to use with heterogeneous results and step-by-step initialization patterns.

There’s a simple way since Swift 5.5 to create child tasks and await their results using async let declarations.

func chopVegetables() async throws -> [Vegetables]  { /* ... */ }
func marinateMeat() async -> Meat { /* ... */ }
func preheatOven(temperature: Int) async -> Oven { /* ... */ }

func makeDinner() async throws -> Meal {
    async let veggies = chopVegetables()
    async let meat = marinateMeat()
    async let oven = preheatOven(temperature: 350)

    let dish = Dish(ingredients: await [try veggies, meat])
    return try await oven.cook(dish, duration: .hours(3))
}

async let is similar to a let, in that it defines a local constant that is initialized by the expression on the right-hand side of the =. However, it differs in that the initializer expression is evaluated in a separate, concurrently-executing child task.

The child task begins running as soon as the async let is encountered. By default, child tasks use the global, width-limited, concurrent executor, in the same manner as task group child-tasks do. On normal completion, the child task will initialize the variables in the async let.

The resulting task is a child task of the currently executing task. Because of this, and the need to suspend to await the results of such expression, async let declarations may only occur within an asynchronous context, i.e. an async function or closure.

For single statement expressions in the async let initializer, the await and try keywords may be omitted. The effects they represent carry through to the introduced constant and will have to be used when waiting on the constant.

It is illegal to declare an async var. This is due to the complex initialization that a async let represents, it does not make sense to allow further external modification of them.