Now that I'm living completely in a Swift 6 async/await
world, I've suddenly hit a snag. When writing code of this sort (never mind what it does, just look at the form of the thing):
let result = services.currentPlaylist.list.filter {
services.download.isDownloaded(song: $0)
}
I'm brought up short by the compiler, which says:
Call to actor-isolated instance method
isDownloaded(song:)
in a synchronous main actor-isolated context
Well, the compiler is right; services.download
is, in fact, an actor. So now what? I can't say await
here:
let result = services.currentPlaylist.list.filter {
await services.download.isDownloaded(song: $0)
}
That just nets me a different error:
Cannot pass function of type
(SubsonicSong) async -> Bool
to parameter expecting synchronous function type
What am I supposed to do here? I can't find an async/await
version of filter
, except on an AsyncSequence. But services.currentPlaylist.list
is not an AsyncSequence; it's an array. And even worse, I cannot find any easy way convert an array to an AsyncSequence, or an AsyncSequence to an array.
Of course I could just solve this by dropping the use of filter
altogether and doing this the "stupid" way, i.e. by looping through the original array with for
. At one point I had this:
var songs = services.currentPlaylist.list
for index in songs.indices.reversed() {
if await services.download.isDownloaded(song: songs[index]) {
songs.remove(at: index)
}
}
But that's so ugly...
Now that I'm living completely in a Swift 6 async/await
world, I've suddenly hit a snag. When writing code of this sort (never mind what it does, just look at the form of the thing):
let result = services.currentPlaylist.list.filter {
services.download.isDownloaded(song: $0)
}
I'm brought up short by the compiler, which says:
Call to actor-isolated instance method
isDownloaded(song:)
in a synchronous main actor-isolated context
Well, the compiler is right; services.download
is, in fact, an actor. So now what? I can't say await
here:
let result = services.currentPlaylist.list.filter {
await services.download.isDownloaded(song: $0)
}
That just nets me a different error:
Cannot pass function of type
(SubsonicSong) async -> Bool
to parameter expecting synchronous function type
What am I supposed to do here? I can't find an async/await
version of filter
, except on an AsyncSequence. But services.currentPlaylist.list
is not an AsyncSequence; it's an array. And even worse, I cannot find any easy way convert an array to an AsyncSequence, or an AsyncSequence to an array.
Of course I could just solve this by dropping the use of filter
altogether and doing this the "stupid" way, i.e. by looping through the original array with for
. At one point I had this:
var songs = services.currentPlaylist.list
for index in songs.indices.reversed() {
if await services.download.isDownloaded(song: songs[index]) {
songs.remove(at: index)
}
}
But that's so ugly...
Share Improve this question edited Mar 26 at 0:08 matt asked Mar 25 at 21:08 mattmatt 537k93 gold badges934 silver badges1.2k bronze badges 3 |4 Answers
Reset to default 3You can use AsyncSyncSequence
from Swift Async Algorithms, like this:
let result = services.currentPlaylist.list.async.filter {
// ^^^^^^
await services.download.isDownloaded(song: $0)
}
This is similar to your answer, but it also handles task cancellation correctly.
To turn the result back to a regular array, you can use reduce
instead of a loop:
extension AsyncSequence {
func toArray() async rethrows -> [Element] {
try await reduce(into: []) { $0.append($1) }
}
}
There is also CollectionConcurrencyKit from John Sundell.
It's simple code which gives nice methods.
For instance, asyncMap()
is like that:
func asyncMap<T>(
_ transform: (Element) async throws -> T
) async rethrows -> [T] {
var values = [T]()
for element in self {
try await values.append(transform(element))
}
return values
}
Nothing fancy, nothing complicated, just do the job directly on Sequence
.
I heard you were looking for reduce(into:_:)
:
A possible solution (not tested), but by combining the simple logic of CollectionConcurrencyKit and the source code of the method:
func asyncReduce<Result>(
into initialResult: Result,
_ updateAccumulatingResult: (inout Result, Self.Element) async throws -> ()
) async rethrows -> Result {
var accumulator = initialResult
for element in self {
try await updateAccumulatingResult(&accumulator, element)
}
return accumulator
}
Some people suggest using task groups:
func allDownloaded(in data: [String]) async -> [String] {
await withTaskGroup(of: (String?).self) { group in
for item in data {
group.addTask {
await isDownloaded(item) ? item : nil
}
}
return await group.reduce(into: []) { array, result in
if let result {
array.append(result)
}
}
}
}
func isDownloaded(_ item: String) async -> Bool {
return item.count > 3
}
But I think @matt solution is better since it's reusable
I never found any built-in solution, so I ended up writing my own conversions:
struct SimpleAsyncSequence<T: Sendable>: AsyncSequence, AsyncIteratorProtocol, Sendable {
private var sequenceIterator: IndexingIterator<[T]>
init(array: [T]) {
self.sequenceIterator = array.makeIterator()
}
mutating func next() async -> T? {
sequenceIterator.next()
}
func makeAsyncIterator() -> SimpleAsyncSequence { self }
}
extension AsyncSequence where Element: Sendable {
func array() async throws -> [Element] {
var result = [Element]()
for try await item in self {
result.append(item)
}
return result
}
}
Or, for that extension, I could write it like this (basically as suggested here):
extension AsyncSequence where Element: Sendable {
func array() async throws -> [Element] {
try await reduce(into: []) { $0.append($1) }
}
}
Now my code can talk like this:
let sequence = SimpleAsyncSequence(array: services.currentPlaylist.list).filter {
await services.download.isDownloaded(song: $0)
}
let result = try await sequence.array()
But I really don't know whether this is the best approach, and I remain surprised that the Swift library doesn't make this a whole lot simpler somehow (and would be happy to hear that it does).
Update The other answers confirmed that, incredibly, no solution is built-in to the library as currently shipping, so I ended up keeping my approach. It's great to know that other solutions are out there, but I don't want my app to use any third-party dependencies.
extension Sequence { func asyncReduce<Result>( into initialResult: Result, _ updateAccumulatingResult: (inout Result, Self.Element) async throws -> () ) async rethrows -> Result { var accumulator = initialResult for element in self { try await updateAccumulatingResult(&accumulator, element) } return accumulator } }
might work by checking also the source code github/swiftlang/swift/blob/… – Larme Commented Mar 26 at 11:36async
methods formap
andfilter
etc., rather than requiring that we turn an array into an async sequence, do the stuff, and turn the async sequence back into an array. This is what I expected Apple to have done for us by now. I realize that in a sense this is "just a link" but still, if you'd give this as an answer (feel free to expand on it), I'll upvote it. – matt Commented Mar 26 at 16:54