Async sort(by:)

I'm trying to sort a collection using sorted(by:), but the closure requires an async function. Something similar to this:

let values = [Value]()
let sorted = values.sorted { left, right in 
    await asyncFunction(left, right)  // error

This won't compile because sorted(by:) doesn't accept an async closure. Other sequence methods like map, filter, and reduce are pretty easy to solve the issue. You could create an AsyncSequence and use its methods or create a fairly straightforward extension that accepts an async closure. However, sorting is not implemented for AsyncSequence (which makes sense). I could create my own sorting method, but that's a pretty tall order when all I want to do is use the existing algorithm with an async closure.

Is there a way to shim an async closure into a method like sorted(by:) that expects a synchronous closure?

1 Like

Not currently. We've talked about introducing a reasync feature that would make it easier for the standard library to allow this sort of thing, but for the time being, there's no way to do it.

Out of curiosity, what is your sort order doing that requires async work?

The reasync idea was exactly what I was thinking of, so consider this a vote for that.

My domain for sorting is a bit complex, but here goes. I have a custom language using an interpreter where I was experimenting with pushing values out to an actor instead of synchronizing with a dispatch queue. This required adding async conformance to a number of methods in the interpreter. There's a sort function in my language that uses swift's native sorting, but it needs to run a provided closure through the interpreter as the sort logic. Thus, I would need the sorted(by:) closure to allow an async call. For the meantime, I can go back to using a dispatch queue for synchronization until there's a more general solution.

I do think the issue is larger than just sorting, though. As I've been trying out async/await, I've found that a number of my own methods needed to have async versions added where the only difference is the signature of the provided closure. For example, here's an initializer on Dictionary that's been useful:

init<S>(_ sequence: S, 
        key: KeyPath<S.Element, Key>, 
        value valueGenerator: (S.Element) throws -> Value, 
        uniquingKeysWith combine: (Value, Value) -> Value) rethrows
where S: Sequence

If valueGenerator needs access to an async closure, I have to declare another version of the initializer that's identical except for using await when calling valueGenerator. Having reasync along with rethrows would be very helpful.

1 Like

I was wondering if that was what you were doing. Dispatching to an actor in a sort comparator is not a good idea, partly for performance but mostly because you are allowing arbitrary code to interleave on the actor while you’re sorting, which can presumably change the ordering while you’re sorting. You need to be thinking about these things “transactionally”, and in this case that means (at a minimum) you need to dispatch to the actor in order to do the complete sort.

1 Like

That sounds like good advice. This is a bit of a special case where I don't think it's an issue, but then it probably doesn't need to be an actor either. Just getting my arms around the concepts of the concurrency model. Thanks for the guidance, and I'll have my eyes peeled for a reasync keyword :wink: