I propose the addition of async
/ await
to handle asynchronous operations in a manner that is consistent with Swift without a monad like the Promise
in JavaScript and the Task
in C#.
Handling asynchronous operations is an area of great interest and async
/ await
are often referred to in swift-evolution. After some thought, I realized that if you consider async
and await
as analogous to throws
and try
, a solution that fits quite well with Swift emerges.
For example, functions calling functions marked with throws
must themselves be marked with throws
or handle the error explicitly using do
/ catch
. In the same way, functions which call async
functions must themselves be async
or handle the asynchrony explicitly using do
/ wait
. Just like calling a throwing function requires it to be annotated with try
, calling an async
function it to be annotated with await
.
// An `async` function
func downloadData(from url: URL) async -> Data { ... }
// can be used as following
func downloadFoo(from url: URL) async -> Foo { // Must be `async`
let data = await downloadData(from: url) // Needs `await`
return try! JSONDecoder().decode(Foo.self, from: data)
}
// or
func downloadFoo(from url: URL) -> Foo { // Without `async`
do {
let data = await downloadData(from: url) // Needs `await`
return try! JSONDecoder().decode(Foo.self, from: data)
} wait // Blocking
}
async
/ await
and throws
/ try
can be used together for asynchronous failable operations.
// An `async` and `throws` function
func downloadData(from url: URL) async throws -> Data { ... }
// can be used as following
func downloadFoo(from url: URL) async throws -> Foo { // Must be `async` and `throws`
let data = await try downloadData(from: url) // Needs Both `await` and `try`
return try JSONDecoder().decode(Foo.self, from: data)
}
// or
func downloadFoo(from url: URL) async -> Foo { // Can be only `async`
do {
let data = await try downloadData(from: url) // Needs Both `await` and `try`
return try JSONDecoder().decode(Foo.self, from: data)
} catch _ {
return Foo()
} // Without `wait`
}
// or
func downloadFoo(from url: URL) throws -> Foo { // Can be only `throws`
do {
let data = await try downloadData(from: url) // Needs Both `await` and `try`
return try JSONDecoder().decode(Foo.self, from: data)
} wait // Without `catch`
}
// or
func downloadFoo(from url: URL) -> Foo { // Without `async` or `throws`
do {
let data = await try downloadData(from: url) // Needs Both `await` and `try`
return try JSONDecoder().decode(Foo.self, from: data)
} catch _ {
return Foo()
} wait
}
Also it is possible to think about a reasync
which is analogous to rethrows
.
func map<T>(_ transform: (Element) async throws -> T) reasync rethrows -> [T] { ... }
// behaves as an `async` function only when a received closure is `async`
let urls = [...]
let foos = await try urls.map { await try downloadFoo($0) }
async
and await
can work with GCD, OperationQueue
, Thread
and so on. To accomplish this, the standard library could provide an asynchronize
function like below.
func asynchronize(_ operation: (@escaping ((T) throws -> ()) -> ()) -> ()) async rethrows -> T { ... }
// Implements an `async` function by GCD
func asyncAfter<T>(deadline: DispatchTime, execute: () -> T) async -> T {
return asynchronize { resolve in
DispatchQueue.main.asyncAfter(deadline: deadline) {
resolve { execute() }
}
}
}
The following nonblock
function would also be useful to make it easy to realize nonblocking operations.
func nonblock(_ operation: () async -> ()) { ... }
// Implements nonblocking operations
@IBAction func onPressButton() {
nonblock { // This closure is `() async -> ()`
do {
let data = await try downloadData(from: url)
return try JSONDecoder().decode(Foo.self, from: data)
} catch _ {
return Foo()
} // No `wait` here and it makes this closure `async`
}
}
Never used gists with forks. Just in case you can't see, I made a fork at https://gist.github.com/rayfix/b5429a061a0fe34fa9326fe358dfdbe7
This fork doesn't change any of the ideas (or code) but just fixes some of the language. I think when you are introducing the concept it is better to first talk about the well known language construct throws / try and then compare to the new construct async / await rather than the other way around.
One thing that I wonder about is how it composes though.
For example:
But for async you can't do that well:
In this case, it doesn't
wait
because it marked the functionasync
. But it really has to because the data dependency. So in that way it is a little different thanthrows
/try