My Problem π€
When processing collections with asynchronous transformations, the standard map method falls short because its closures cannot be async.
A naive solution would be to use a for loop with await, but this sacrifices readability and consistent error handling.
The goal is to have a sequential map that accepts async throws functions, preserves element order, and simplifies the workflow.
My Solution π§©
We create an extension on Sequence that adds asyncMap.
Its execution is sequential within the same task, which allows us to:
- Maintain deterministic ordering of results.
- Limit resource consumption by executing only one operation at a time.
- Uniformly propagate the first error that occurs, stopping the process.
extension Sequence {
@inlinable
func asyncMap<T>(
_ transform: @escaping @Sendable (Element) async throws -> T
) async throws -> [T] {
var results: [T] = []
results.reserveCapacity(underestimatedCount)
for element in self {
let value = try await transform(element)
results.append(value)
}
return results
}
}
My Result π―
With asyncMap, we achieve a clear and safe workflow for asynchronous transformations without parallelism.
Itβs especially useful when:
- We need guaranteed ordering.
- We must limit resource consumption (one task in flight).
- Thereβs dependency between operations.
If you need parallel execution instead of sequential, I explored that approach in Concurrent Map. And if your sequential calls hit rate limits, I added a timeout mechanism in Async Map Timeout.
Usage example:
let urls: [URL] = [...]
let contents = try await urls.asyncMap {
try await fetchContent(from: $0)
}
Keep coding, keep running πββοΈ