Async Map

Varios Jorges corriendo por la misma linea uno detras de otros.

Tabla de contenidos


Problema

Cuando procesamos colecciones con transformaciones asíncronas, el método map no sirve porque sus closures no pueden ser async.
Una solución ingenua sería usar un bucle for con await, pero se pierde legibilidad y un manejo de errores homogéneo.
El objetivo es disponer de un map secuencial que acepte funciones async throws, preserve el orden de los elementos y simplifique el flujo de trabajo.


Solución

Se crea una extensión de Sequence que añade asyncMap.
Su ejecución es secuencial dentro de la misma tarea, lo que permite:

  • Mantener el orden determinista de los resultados.
  • Limitar el consumo de recursos ejecutando solo una operación a la vez.
  • Propagar de manera uniforme el primer error que se produzca, deteniendo el proceso.
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
    }
}

Resultado

Con asyncMap se obtiene un flujo claro y seguro para transformaciones asíncronas sin paralelismo.
Es especialmente útil cuando:

  • Necesitamos orden garantizado.
  • Debemos limitar el consumo de recursos (una tarea en vuelo).
  • Existe dependencia entre las operaciones.

Si necesitas ejecución en paralelo en lugar de secuencial, exploré ese enfoque en Concurrent Map. Y si tus llamadas secuenciales alcanzan límites de tasa, añadí un mecanismo de timeout en Async Map Timeout.

Ejemplo de uso:

let urls: [URL] = [...]
let contents = try await urls.asyncMap {
    try await fetchContent(from: $0)
}

Keep coding, keep running 🏃‍♂️