I'll add a few notes of my own to the review 
A few of the things I've been pondering:
withGroup parameter names
Today we do:
public static func withGroup<TaskResult, BodyResult>(
resultType: TaskResult.Type,
returning returnType: BodyResult.Type = BodyResult.self,
body: (inout Task.Group<TaskResult>) async throws -> BodyResult
) async rethrows -> BodyResult {
I would like to propose:
public static func withGroup<ChildTaskResult, GroupResult>(
childResult: ChildTaskResult.Type,
returning returnType: GroupResult.Type = GroupResult.self,
body: (inout Task.Group<ChildTaskResult>) async throws -> GroupResult
) async rethrows -> GroupResult {
or even more fluently:
public static func withGroup<ChildTaskResult, GroupResult>(
aggregating: ChildTaskResult.Type, // or collecting?
returning returnType: GroupResult.Type = GroupResult.self,
body: (inout Task.Group<ChildTaskResult>) async throws -> GroupResult
) async rethrows -> GroupResult {
resulting in this nicer to read shape:
await Task.withGroup(aggregating: Int.self) { group in
await group.add { 1 }
}
I'd welcome nice spelling ideas for all those parameters.
Should group.next() assert that it is called correctly?
It is by design and important for performance and correctness of a group that next() be only invoked from the task running the group.
We could assert(Task.unsafeCurrent == _task) check in next() if we really wanted to, but it feels like quite a performance hit to be hitting current task for every single next() call so I'm on the fence about it. It's also why I was considering (and currently doing so) only checking this in debug mode.
The next() function is on purpose declared mutating in order to make it hard to use it incorrectly by escaping it etc. The group is NOT Sendable.
Any opinions on this one?
Suspending group.add
Food for thought - today we always await group.add but we never actually suspend there.
We want to be "future proof" that at some point we'll have groups that control the number of added tasks... but we have not designed this at all. I am wondering if this is future proof, or troublesome.
I do feel like we may need various types of "add" eventually. Good names I had in mind were things like launch, spawn etc. But we'd need to align those with whatever async lets end up doing etc.
Just bringing it up for reviewers to consider.
Following up on this one:
Yeah so that's true, and I'll adjust that, however in practice it does not win us much, because we're unable to correlate (not way to express in type-system), the following relation:
// (NOT REAL API)
await group.add { 1 }
// "aha, we never throw in any of the child tasks!"
// thus...
let one = /*try*/ await next() // we don't have to throw from next
So next() always is throwing, because we don't have a way to relate it to the fact that no child tasks would have had the ability to throw. So realistically, the vast majority of group implementations will be throwing, because they have to throw out of the next().
People could try? await next() or try! await next(), so yes the rethrows on the withGroup does matter, but sadly won't completely solve the sometimes throwing even if we know we won't issue.
The same is true with consuming the group as an async sequence:
for try await r in group { // forced to try since next() throwing
}
We could solve it the same way as we do with UnsafeContinuation, by duplicating the types:
await Task.withThrowingGroup(accumulating: Int.self) { group in ...
var sum = 0
for n in 1...10 { await group.add { return n } }
for await r in group { sum += r } // no try!
return sum
}
try await Task.withThrowingGroup(accumulating: Int.self) { group in ...
var sum = 0
for n in 1...10 { await group.add { throw Boom() } }
for try await r in group { sum += r }
return sum
}
do we want to do this though?
(Just adding a Failure parameter does not solve it, because we have no way to "hide" the add(body: throws async) function from groups which are Failure = Never even if we can hide the not throwing next()).