arraysswiftasync-awaithigher-order-functions

Swift array `filter`, `map`, etc. (higher-order functions) when the closure needs to be `async`


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...


Solution

  • 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.