[Pitch] Full async overload support

With Swift 6 on the horizon, I wonder if it's worth revisiting how async methods and functions are chosen. Currently, they are chosen based on the context only, where two methods that differ in async only are not technically overloads.

For instance, in Swift 5.10, you would end up with the following:

func function() { print("sync") }
func function() async { print("async") }

func syncContext() {
    function() // prints sync
    await function() // error: 'await' in a function that does not support concurrency
}
func asyncContext() async {
    function() // error: expression is 'async' but is not marked with 'await'
    await function() // prints async
}

I expect that most users of Swift view the two versions of function() as overloads (even though they aren't), and thus the resulting error in the asyncContext() case above is perplexing.

I propose that we consider fully supporting methods that differ only in async as proper overloads of one another, allowing await to choose between those overloads:

func syncContext() {
    function() // prints sync
    await function() // error: 'await' in a function that does not support concurrency
}
func asyncContext() async {
    function() // prints sync
    await function() // prints async
}

As I understand it, there may be some edge cases in actors since the async keyword is not always necessary to declare, while using the actor in a different context would always require await, so this is likely language breaking to some extent, but I believe it's usability and expectation improvements for the vast majority of other situations would be worth reworking.

Closures

The number one situation this came up for me if for functions that take "non escaping" closures of some sort:

func process(with closure: () -> ()) {
    print("sync")
    closure()
}
func process(with closure: () async -> ()) async {
    print("async")
    await closure()
}

A way this manifests is in things like test assertions, where once you add an async overload to one of them, you are forced to always use the async overload, even for closures that are non-async.

No-async

Another case where this may improve the situation is with @available(*, noasync) declarations, where both the sync and async versions of a function can share a name, but once again be differentiated by context only:

@available(*, noasync)
func function() { print("sync") }
func function() async { print("async") }

func syncContext() {
    function() // prints sync
    await function() // error: 'await' in a function that does not support concurrency
}
func asyncContext() async {
    function() // warning: expression is unavailable from asynchronous contexts; this is an error in the Swift 6 language mode
    await function() // prints async
}
4 Likes

I've hit this problem a couple of times. It's not been hard to workaround, but it would be nice if I didn't have to.

At the very least, I wish the compiler's "error" messages were more informative, i.e. pointing out that it does recognise the existence of the sync version of function but it's not letting you call it directly. (though, that of course just highlights that it's a seemingly arbitrary and capricious restriction, from the end-user's perspective, so just fixing it would be the best option)

5 Likes