[Pitch] Concurrent Async Iteration over Parameter Packs

Introduction

When using parameter packs and async functions, I identified some potential gaps in the current syntax for concurrent iteration over parameter packs.

Motivation

Consider fetching data with an async function:

protocol Fetchable: Sendable
{
    associatedtype Output: Sendable
    func fetch() async -> Output
}

func fetchAll<each Item: Fetchable>(items: repeat each Item) async -> (repeat (each Item).Output)
{
    // We want to call fetch() on ALL items concurrently
}

Limitation with TaskGroup

await withTaskGroup(of: Int.self) { group in
    for item in repeat each items {
        group.addTask { await item.fetch() }
    }
    // ...
}

TaskGroup is concurrent, but it's dynamic and requires a single, homogeneous return type. This is fine when all items return the same type — but parameter packs are often heterogeneous.

// StringFetcher.fetch() -> String
// IntFetcher.fetch() -> Int
// BoolFetcher.fetch() -> Bool

func fetchAll<each T: Fetchable>(_ items: repeat each T) async -> ??? {
    // We want: (String, Int, Bool) — but TaskGroup can't express this!
}

Illusion of await (repeat (each items).fetch())

Current Swift syntax for iterating over packs produces sequential execution:

await (repeat (each items).fetch())
// OR
(repeat await (each items).fetch())
// Each item is awaited one-by-one — no concurrency!

Even though the compiler knows exactly how many children exist at compile time, there's no way to express "await all of these concurrently."

Possible Solution: await Unstructured Tasks

let tasks = (repeat Task { await (each items).fetch() } )
let results = await (repeat (each tasks).value)

But this is unstructured concurrency, and parameter packs provide very useful static type information. This type information can be potentially used by structured concurrency to expand its capabilities.

And this is exactly the gap this pitch is trying to fill.

Summary: The Gap

Approach Concurrent? Static Types? Heterogeneous? Structured?
TaskGroup :white_check_mark: Yes :cross_mark: Dynamic :cross_mark: Homogeneous only :white_check_mark: Yes
repeat await :cross_mark: Sequential :white_check_mark: Yes :white_check_mark: Yes :white_check_mark: Yes
repeat Task { ... } :white_check_mark: Yes :white_check_mark: Yes :white_check_mark: Yes :cross_mark: Unstructured

Proposed Solution

Introduce repeat async to signal concurrent spawning of pack operations:

// Sequential (current behavior)
let results = await (repeat (each items).fetch())

// Concurrent (proposed)
let results = await (repeat async (each items).fetch())

This proposes an expression that tells the compiler "a heterogeneous value is being concurrently constructed."

Example: Heterogeneous Return Types

import Foundation

protocol Fetchable: Sendable 
{
    associatedtype Output: Sendable
    func fetch() async -> Output
}

struct StringFetcher: Fetchable 
{
    let string: String
    func fetch() async -> String 
    { 
        try? await Task.sleep(for: .seconds(1))
        return string 
    }
}

struct IntFetcher: Fetchable 
{
    let integer: Int
    func fetch() async -> Int 
    { 
        try? await Task.sleep(for: .seconds(1))
        return integer 
    }
}

struct DoubleFetcher: Fetchable 
{
    let double: Double
    func fetch() async -> Double 
    { 
        try? await Task.sleep(for: .seconds(1))
        return double 
    }
}

func fetchAll<each Item: Fetchable>(items: repeat each Item) async -> (repeat (each Item).Output) 
{
    // TaskGroup can't work here because it requires a single, homogeneous return type
    
    // Sequential
    // (repeat await (each items).fetch())
    // OR
    // await (repeat (each items).fetch())

    // Unstructured concurrency
    let tasks = (repeat Task { await (each items).fetch() } )
    let results = await (repeat (each tasks).value)
    return results

    // Structured Concurrent (Hypothetical)
    // await (repeat async (each items).fetch())
}



@main
struct Main 
{
    static func main() async 
    {
        let stringFetcher = StringFetcher(string: "Hello")
        let intFetcher = IntFetcher(integer: 42)
        let doubleFetcher = DoubleFetcher(double: 3.14)
        
        print("Fetching...")
        let start = Date()
        let results = await fetchAll(
            items: stringFetcher, intFetcher, doubleFetcher
        )
        let elapsed = Date().timeIntervalSince(start)
        
        print(results)
        print("Elapsed: \(String(format: "%.2f", elapsed))s")
    }
}