Article directory
foreword
AsyncSequence
Part of the Concurrency Framework and SE-298 proposal. Its name implies that it is a type that provides asynchronous, sequential and iterative access to its elements. In other words: it's an asynchronous variant of the regular sequences we're familiar with in Swift.
Just like you don't create your custom sequence very often, I don't expect you to create a custom AsyncSequence
implementation . However, you will most likely have to work with asynchronous sequences due to use with types AsyncThrowingStream和AsyncStream
such as . Therefore, I will guide you to work with AsyncSequence
examples .
What is AsyncSequence?
AsyncSequence
is an asynchronous variant Sequence
of . Due to its asynchronous nature, we need to use await
the keyword since we are dealing with methods defined asynchronously. If you haven't used it async/await
, I encourage you to read my article: async/await in Swift - code examples explained
Values can become available over time, meaning that a AsyncSequence
value may or may not contain some, or all, values the first time you use it.
It's important to understand AsyncSequence
that is just a protocol. It defines how to access the value, but does not produce or contain the value. AsyncSequence
The implementor of the protocol provides one AsyncIterator
and is responsible for developing and potentially storing the value.
Function | Note |
---|---|
contains(_ value: Element) async rethrows -> Bool |
Requires Equatable element |
contains(where: (Element) async throws -> Bool) async rethrows -> Bool |
The async on the closure allows optional async behavior, but does not require it |
allSatisfy(_ predicate: (Element) async throws -> Bool) async rethrows -> Bool |
|
first(where: (Element) async throws -> Bool) async rethrows -> Element? |
|
min() async rethrows -> Element? |
Requires Comparable element |
min(by: (Element, Element) async throws -> Bool) async rethrows -> Element? |
|
max() async rethrows -> Element? |
Requires Comparable element |
max(by: (Element, Element) async throws -> Bool) async rethrows -> Element? |
|
reduce<T>(_ initialResult: T, _ nextPartialResult: (T, Element) async throws -> T) async rethrows -> T |
|
reduce<T>(into initialResult: T, _ updateAccumulatingResult: (inout T, Element) async throws -> ()) async rethrows -> T |
For these functions, we first define a type that conforms to AsyncSequence
the protocol . The name is modeled after existing standard library "sequence" types, such as LazyDropWhileCollection
and LazyMapSequence
. We AsyncSequence
then add a function in the extension of that creates the new type (using 'self' as 'upstream') and returns it.
Function |
---|
map<T>(_ transform: (Element) async throws -> T) -> AsyncMapSequence |
compactMap<T>(_ transform: (Element) async throws -> T?) -> AsyncCompactMapSequence |
flatMap<SegmentOfResult: AsyncSequence>(_ transform: (Element) async throws -> SegmentOfResult) async rethrows -> AsyncFlatMapSequence |
drop(while: (Element) async throws -> Bool) async rethrows -> AsyncDropWhileSequence |
dropFirst(_ n: Int) async rethrows -> AsyncDropFirstSequence |
prefix(while: (Element) async throws -> Bool) async rethrows -> AsyncPrefixWhileSequence |
prefix(_ n: Int) async rethrows -> AsyncPrefixSequence |
filter(_ predicate: (Element) async throws -> Bool) async rethrows -> AsyncFilterSequence |
Create AsyncSequence
Create a custom AsyncSequence.
To better understand AsyncSequence
how works, I'll show an example implementation. However, when defining your custom implementation AsyncSequence
of , you may want to use AsyncStream
instead, as it is more convenient to set up. Therefore, this is just a code example to better AsyncSequence
understand how works.
The following example follows the example in the original proposal and implements a counter. The values are available immediately, so there is not much need for an asynchronous sequence. However, it does show the basic structure of an asynchronous sequence:
struct Counter: AsyncSequence {
typealias Element = Int
let limit: Int
struct AsyncIterator : AsyncIteratorProtocol {
let limit: Int
var current = 1
mutating func next() async -> Int? {
guard !Task.isCancelled else {
return nil
}
guard current <= limit else {
return nil
}
let result = current
current += 1
return result
}
}
func makeAsyncIterator() -> AsyncIterator {
return AsyncIterator(howHigh: limit)
}
}
As you can see, we define AsyncSequence
a Counter
struct that implements the protocol. The protocol requires us to return a custom one AsyncIterator
, which we solved using an internal type. We could decide to rewrite this example to remove the need for the inner type:
struct Counter: AsyncSequence, AsyncIteratorProtocol {
typealias Element = Int
let limit: Int
var current = 1
mutating func next() async -> Int? {
guard !Task.isCancelled else {
return nil
}
guard current <= limit else {
return nil
}
let result = current
current += 1
return result
}
func makeAsyncIterator() -> Counter {
self
}
}
We can now return self
as an iterator and keep all logic centralized.
Note that we must help the compiler comply with AsyncSequence
the protocol .
next()
method is responsible for iterating over the overall value. Our example boils down to feeding as many count values as possible until we hit the limit. We implement cancellation support through a check Task.isCancelled
on .
Iteration of an asynchronous sequence
Now that we know what is AsyncSequence
and how it's implemented, it's time to start iterating over the values.
Using the above example as an example, we can use Counter
to start the iteration:
for await count in Counter(limit: 5) {
print(count)
}
print("Counter finished")
// Prints:
// 1
// 2
// 3
// 4
// 5
// Counter finished
We must use await
the keyword because we may receive values asynchronously. We exit the for loop once there are no more expected values. Implementors of asynchronous sequences next()
can nil
signal reaching the limit by returning in the method. In our case, we hit this expectation once the counter reaches the configured limit, or the iteration cancels:
mutating func next() async -> Int? {
guard !Task.isCancelled else {
return nil
}
guard current <= limit else {
return nil
}
let result = current
current += 1
return result
}
Many regular sequence operators can also be used with asynchronous sequences. As a result, we can perform operations such as mapping and filtering asynchronously.
For example, we can filter only even numbers:
for await count in Counter(limit: 5).filter({
$0 % 2 == 0 }) {
print(count)
}
print("Counter finished")
// Prints:
// 2
// 4
// Counter finished
Or we can map the count to one before iterating String
:
let counterStream = Counter(limit: 5)
.map {
$0 % 2 == 0 ? "Even" : "Odd" }
for await count in counterStream {
print(count)
}
print("Counter finished")
// Prints:
// Odd
// Even
// Odd
// Even
// Odd
// Counter finished
We can even use AsyncSequence
instead of a for loop, by using contains
methods like .
let contains = await Counter(limit: 5).contains(3)
print(contains) // Prints: true
Note that the above method is asynchronous, meaning it has the potential to wait endlessly for a value to exist until the underlying AsyncSequence
completes .
in conclusion
AsyncSequence
is the asynchronous replacement for the Sequence
regular . Just Sequence
as , it's also unlikely that you'd create a custom asynchronous sequence.