New Package: Look at Swift Async Algorithms

Apple released the first version of the async swift algorithms package. It provides tools and algorithms to use with the introduced not that far ago Async Sequence

Alex Dremov
New Package: Look at Swift Async Algorithms

About a month ago, Apple released the first version of the async swift algorithms package. It provides tools and algorithms to use with the introduced not that far ago asynchronous sequence. The package focuses on implementing already well-known tools like zip as well as new features that transact in time (wow). It also makes available more sophisticated ways of creating and managing asynchronous sequences.

💥
The module's latest version is 0.0.1, which means that it's still in development. So, some methods are not available yet, some may change or appear.

Mostly, this article here is to get to know new features and, possibly, plan your code, keeping in mind that such features will appear in the future

Installation

The new package is distributed through Swift PM. To add it to your project, you need to add it as a dependency in the Xcode project File > Add Packages.

Or add it to your Package.swift file:

.package(url: "https://github.com/apple/swift-async-algorithms"),

Don't forget to also add the dependency to the executable:

.target(name: "<target>", dependencies: [
    .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
]),

The module will be available in your project after adding import AsyncAlgorithms.

💥
As I mentioned, the module is still in development. So, you need to install Swift Trunk Development toolchain to have access to all features.
Some of them are available right away, though!

Creating asynchronous sequences

To test all the beautiful functions the new module provides, we need to create an async sequence at first. And the package introduces new ways of doing so.

Property async

The module adds the following extension to Sequence protocol.

extension Sequence {
  public var async: AsyncLazySequence<Self> { get }
}

Where AsyncLazySequence conforms to AsyncSequence.

public struct AsyncLazySequence<Base: Sequence>: AsyncSequence {
}

extension AsyncLazySequence: Sendable where Base: Sendable {
	...
}
extension AsyncLazySequence.Iterator: Sendable where Base.Iterator: Sendable {
}
💡
Using the async property, we can turn any existing Sequence into AsyncSequence to use them in some async API, for example.
let numbers = [1, 2, 3, 4].async
let characters = "Hello, world".async
let items = [1: "one", 2: "two", 3: "three"].async

However, creating AsyncSequence this way does not really bring benefits as all elements are already here and available right away. There are more useful ways of creating AsyncSequence.

Subscribe and don't miss posts!

AsyncChannel and AsyncThrowingChannel

If you know what Future or Promise in other languages are, then AsyncChannel will be familiar to you. Except that it provides a way of transferring a sequence of values.

Channel's element must conform to the Sendable protocol, which basically means that public API is safe to use across concurrency domains.

All basic types automatically conform to it. For custom types, you need to add the conformance before use.

Here's a pretty straightforward example of AsyncChannel usage.

let channel = AsyncChannel<String>()
Task {
    for word in ["Hello", "from", "async", "channel"] {
      await channel.send(word)
    }
    await channel.finish()
}

for await message in channel {
    print(message)
}
Hello
from
async
channel

Notice that await keyword is used with send and finish. This is because the channel is actually both ways synchronized. That means that send awaits consumption and vice versa.

💡
The await channel.send() waits until the sent value will be consumed in any way. This way, the one who produces values for the channel, will not generate more values than the receiver can consume 

AsyncThrowingStream is almost the same except that it provides fail(_ error: Error) method that can be used to throw an exception to the channel's consumer.

let channel = AsyncThrowingChannel<String, Error>()

...

for try await message in channel {
    print(message)
}

And converting back

The module adds initializers for three primary types: Array, Dictionary, and Set that let you transform the async sequence to the regular one by fetching all elements during init.

let table = await Dictionary(uniqueKeysWithValues: zip(keys, values))
let allItems = await Set(items.prefix(10))
let allMessages = await Array(channel)

Manipulating asynchronous sequences

The module also provides new ways of combining asynchronous sequences. These functions are pretty straightforward.

  • chain(_ s1: AsyncSequence, _ s2: AsyncSequence)

Chains two or three asynchronous sequences together sequentially where the elements from the result are comprised in order from the elements of the first asynchronous sequence and then the second (and so on) or until an error occurs. Sequences must have the same Element type.

Sequence 1 Sequence 2 Result
1 1
4 4
2 2
3 3
💥
Apple notes that it can be used for two or more sequences. Though, only two or three arguments are available now. 
  • joined() or joined(separator: AsyncSequence)

Concatenates an asynchronous sequence of asynchronous sequences together where the result is comprised in order from the elements of the first asynchronous sequence and then the second (and so on) or until an error occurs. Similar to chain()except the number of asynchronous sequences to concatenate is not known upfront. The separator also can be specified.

  • combineLatest(_ base1: AsyncSequence, _ base2: AsyncSequence)

Combines two or more sequences, producing tuples of the latest values available from the sequence.

Sequence 1 Sequence 2 Result
1 awaits
2 (1, 2)
3 (1, 3)
4 (4, 3)
  • merge(_ base1: AsyncSequence, _ base2: AsyncSequence)

Merges sequences into a new one. The result is a combination of results from two sequences. Sequences must have the same Element type.

Sequence 1 Sequence 2 Result
awaits
1 1
2 2
3 3
4 4
💡
Considering that it's not defined from which sequence element will appear faster, the order of elements can be whatever
  • zip(_ base1: AsyncSequence, _ base2: AsyncSequence)

The same as a regular zip but for AsyncSequence. Differs from combineLatest as it waits until the second value is available and does not use the last value.

Sequence 1 Sequence 2 Result
1 awaits
2 (1, 2)
3 awaits
4 (4, 3)

Sounds awesome, but Swift is not powerful enough to put await before the time itself. When events can potentially happen faster than the desired consumption rate, there are ways to handle the situation. These functions allow linking AsyncSequences with time. They can be applied to any AsyncSequence.

For both listed methods, a custom clock can be specified. By default, it's ContinuousClock

Debounce

 public func debounce<C: Clock>(
    for interval: C.Instant.Duration, 
    tolerance: C.Instant.Duration? = nil, 
    clock: C
  ) -> AsyncDebounceSequence<Self, C>

The debounce algorithm produces elements after a particular duration has passed between events. If there are a lot of events happening, debounce will wait until at least interval of time elapsed from the last event before emitting value.

seq.debounce(for: .seconds(1))

In this case, it transforms a potentially fast asynchronous sequence of events into one that waits for a window of 1 second with no events to elapse before emitting a value.

Throttle

extension AsyncSequence {
  public func throttle<C: Clock, Reduced>(
    for interval: C.Instant.Duration, 
    clock: C, 
    reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced
  ) -> AsyncThrottleSequence<Self, C, Reduced>
  
  public func throttle<Reduced>(
    for interval: Duration, 
    reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced
  ) -> AsyncThrottleSequence<Self, ContinuousClock, Reduced>
  
  public func throttle<C: Clock>(
    for interval: C.Instant.Duration, 
    clock: C, 
    latest: Bool = true
  ) -> AsyncThrottleSequence<Self, C, Element>
  
  public func throttle(
    for interval: Duration, 
    latest: Bool = true
  ) -> AsyncThrottleSequence<Self, ContinuousClock, Element>
}

The throttle algorithm produces elements such that at least a specific interval has elapsed between them. If values are produced by the base AsyncSequence the throttle does not resume its next iterator until the period has elapsed or unless a terminal event is encountered. Similarly to debounce, a custom clock can be specified.

seq.throttle(for: .seconds(1))

In this case, the throttle transforms a potentially fast asynchronous sequence of events into one that waits for a window of 1 second to elapse before emitting a value.

💡
Notice that debounce, waits for a window with no events, while throttle simply waits for a window.

Final notes

It's actually frankly entertaining to watch how Swift unfolds new features and how they are developed. Definitely check the project's GitHub mentioned in references to check out the module's source code.

If you feel not really confident with relatively new swift concurrency features, check out my quick guide to async/await in Swift.

Quick Guide to Async Await in Swift | Alex Dremov
Everything you need to know about new Swift asynchronous features. Async await, main actor, task, async get, and possible use cases — all covered.

References

GitHub - apple/swift-async-algorithms: Async Algorithms for Swift
Async Algorithms for Swift. Contribute to apple/swift-async-algorithms development by creating an account on GitHub.

Share

Subscribe to Alex Dremov

Get the email newsletter and receive valuable tips to bump up your professional skills