Async forEach (again)

Continuing (and not reviving) an older topic:

Short version: You cannot add async versions of forEach in your own library using the same name, i.e. if you would like to have it, it has to be defined where the original forEach is defined. For me this is a good reason to have async versions of forEach (throwing and not throwing) be defined inside the standard library.

More details:

+1

See tera's comment for an argument why one could like to use it.

You cannot add it outside the standard library (where forEach is defined) with the same name, you have to use a different name in another package (see there for a possible explanation).

Sounds trivial to me.

await [1, 2, 3].async.forEach { n in await showAsync(n) }
               ^~~~~~ convert to `AsyncSyncSequence`

This can easily live in the async-algorithms package, as the implementation should be very simple.

+1 for me as this is a no brainer.

I have the following definitions in my mind:

extension Sequence {
    
    func forEach (
        _ operation: (Element) async -> Void
    ) async {
        for element in self {
            await operation(element)
        }
    }
    
    func forEach (
        _ operation: (Element) async throws -> Void
    ) async rethrows {
        for element in self {
            try await operation(element)
        }
    }
}

...and you cannot add those definitions to the async-algorithms package, you have to add them to the standard library (if you do not want to change the names).

forEach method would go to AsyncSequence not Sequence.

extension AsyncSequence {
  func forEach(...) async rethrows { ... }
}

This one is really trivial and it can live inside the async-algorithms package. ;)

let myArray = ["a", "b"]

let asyncSequence = myArray.async // produces `AsyncSyncSequence`

// inside async scope
await asyncSequence.forEach { ... }

// as a single chain
await ["a", "b"].async.forEach { ... }

Edit: I removed the mentioned AsyncForEachSequence as it's really not needed.

Hmm. The forEach I mentioned has two benefits:

  • You do not need the async algorithms package.
  • Less surprise to a less informed user of an according library: If you use forEach and then "coincidentally" call an async function inside the according closure, the compiler just tells you twice that you should use await and you are happy.

But I also see the advantage of the async property as a general way to find your way to an async sequence giving you also an async map etc.

So I see your point.

Long story short: forEach would be useful for async transformations and algorithms as the final consumption alternative to a for loop. It's easier for everyone to simply extend it in that package and require the user to transform a synchronous sequence into an asynchronous one before he/she can use the async forEach. Otherwise this would require the regular forEach to be extended to func forEach(_ operation: (Element) async throws -> Void) reasync rethrows. Notice the reasync there!

It's a win-win situation if it's added to the package. However I cannot speak for everyone and I think reasync is eventually coming anyways, so :man_shrugging:.

1 Like

If I understand correctly, this is an overload resolution problem. I have two solutions for you:

  1. Add @_disfavoredOverload to your async forEach.
  2. Include your own regular forEach alongside your async version.

@_disfavoredOverload is an underscored attribute, so it's not an official, source-stable language feature and using it is not very cool, but the alternative is to rewrite a built-in sequence method, which also isn't brilliant. :scales:

func test(x: [Int]) async {
    x.forEach { print($0) }
    await x.forEach { await someAsyncFunction($0) }
}

func someAsyncFunction<T>(_: T) async {}

// Change to 'false' to try option 2.
#if true
    extension Sequence {
        @_disfavoredOverload
        func forEach(_ body: (Element) async -> Void) async {
            for element in self { await body(element) }
        }
    }
#else
    extension Sequence {
        func forEach(_ body: (Element) -> Void) {
            for element in self { body(element) }
        }
        func forEach(_ body: (Element) async -> Void) async {
            for element in self { await body(element) }
        }
    }
#endif

There is some ambiguity with an async forEach function. Do you mean that each application of the closure is async, or do you mean that the applications of the closure are concurrent? Some may infer or need the latter (which is not what has been suggested here).

Historically speaking; adding reasync stuff into the standard library has been blocked on a technical reason - using those annotations creates runtime calls to the _Concurrency library. This ended up making cycles between the swift standard library and the swift concurrency library. For some of the other functions that are very clear they really should be reasync the reason why it has not yet happened is mainly due to the technical limitation.

The .forEach function has an added wrinkle; it is viewed by some as superfluous, an optimizer hazard, as well as a cognitive hazard. So those concerns probably should be foremost before adding features to it.

Well, this could be a differentiator between both use cases:

  • β€œregular” sequence with forEach, map etc.: process one element after the other, but allow some awaits within the closure, meaning the processing of the whole sequence might be suspended, nothing happening concurrently;
  • mySequence.async.forEach: When getting e g. an AsyncLazySequence I would at least consider that something more tricky could happen β€” I mean why else the ceremony instead of just adding versions of forEach, map etc. to the standard library allowing an async closure? (It is a serious question, my understanding of the Swift Async Algorithms package is quite superficial at the moment.)

I also need the first use case for a library where concurrent use of the elements of an according sequence is forbidden. But you might want to call an async function, e.g. get data from a database for each element (and applying the data to the element).