Introducing Swift Async Algorithms

As part of Swift’s move toward safe, simple, and performant asynchronous programming, we are pleased to introduce a new package of algorithms for AsyncSequence. It is called Swift Async Algorithms and it is available now on GitHub.

This package has three main goals:

Motivation

AsyncAlgorithms is a package for algorithms that work with values over time. That includes those primarily about time, like debounce and throttle, but also algorithms about order like combineLatest and merge. Operations that work with multiple inputs (like zip does on Sequence) can be surprisingly complex to implement, with subtle behaviors and many edge cases to consider. A shared package can get these details correct, with extensive testing and documentation, for the benefit of all Swift apps.

The foundation for AsyncAlgorithms is already included in Swift 5.5 in AsyncSequence. Swift 5.5 also brings the ability to use a natural for/in loop with await to process the values in an AsyncSequence and Sequence-equivalent API like map and filter. Structured concurrency allows us to write code where intermediate state is simply a local variable, try can be used directly on functions that throw, and generally treat the logic for asynchronous code similar to that of synchronous code.

We believe an open source package will provide a great home for these APIs. A package gives developers flexibility in deploying across both platforms and OS versions. Development and API design will take place on GitHub and the Swift Forums.

A Brief Tour

The package includes AsyncSequence versions of familiar algorithms such as:


Let’s start with a look at zip. Like its Sequence counterpart, zip produces tuples made up of values from two different AsyncSequences:

for await (number, letter) in zip(numbers, letters) {
    print(number, letter)
}

AsyncSequence supports rethrows, which means that error handling is as simple as using try – the same as any other Swift code:

do {
    for try await (number, letter) in zip(numbers, lettersWithErrors) {
        print(number, letter)
    }
} catch {
    // Handle error
}

Other algorithms like combineLatest and merge also combine the output of several AsyncSequences. Each offers a different kind of control over the type and timing of the output.


A fundamental difference between Sequence and AsyncSequence is the introduction of the variable of time. Building on top of proposals to standardize Clock and Duration, the package adds algorithms like debounce and throttle. They provide easy, out-of-the-box solutions for common operations like throwing away values that arrive too fast:

for await value in input.debounce(for: .seconds(0.5)) {
    // Handle input, at most once per 0.5 seconds.
}

It is often useful to wait for the collection of all values in a finite asynchronous sequence. This package provides initializers that do this with a single line of code:

let result = await Array(input)

The async function is useful for combining synchronous Sequences with AsyncSequence. Here, we use it alongside the chain function to add a preamble to the contents of a file:

let preamble = [
    "// This source file is part of the Swift.org open source project"
    "//"
    ""
].async

let lines = chain(preamble, URL(fileURLWithPath: "/tmp/Sample.swift").lines)

for try await line in lines {
    print(line)
}

Combine

Apple introduced the Combine framework in the iOS 13 and macOS 10.15 SDKs. Since then, we’ve had the opportunity to learn how Combine has been used in real-world scenarios. With AsyncAlgorithms, we are applying these lessons as well as embracing the new structured concurrency features of Swift.

Combine’s API is based on the Publisher and Subscriber interfaces, with operators to connect between them. Its design focuses on providing a way to declaratively specify a chain of these operators, transforming data as it moves from one end to the other. This requires thinking differently about intermediate state. Sometimes this leads to call sites that are more complex than one might expect – especially when working with single values, errors, or data that needs to be shared. async/await’s Structured Concurrency provides us with a new way to express this kind of logic. We can now write asynchronous code that is split into smaller pieces and reads from top-to-bottom instead of as a series of chained transforms.

We’re excited about the possibilities that async/await and AsyncSequence bring to the language. We believe this package will be a great place to explore future development and evolution of higher-level APIs in this space.

What’s Next

Today we are releasing a prototype version of the Swift Async Algorithms package. Our intent is to kickstart the project with a working implementation, then move forward with detailed design discussions on the Swift Forums. We welcome community involvement in:

We are using GitHub Issues to track bugs, feature requests, and starter tasks.

References

Documentation

Companion Packages