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
}