Skip to content

Instantly share code, notes, and snippets.

@koher
Last active October 20, 2017 02:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save koher/3e04b4f1b8adbbf0379d38c0ad83a3ea to your computer and use it in GitHub Desktop.
Save koher/3e04b4f1b8adbbf0379d38c0ad83a3ea to your computer and use it in GitHub Desktop.

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`
        }
    }
@rayfix
Copy link

rayfix commented May 6, 2017

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:

func getModels() throws -> [Model] {
   let data = try download() // this can throw
   return try parse(data)  // this throws too
}

But for async you can't do that well:

func getModels() async throws -> [Model] {
  let data = await try download()
  return await try parse(data)  // you really need to wait here
}

In this case, it doesn't wait because it marked the function async. But it really has to because the data dependency. So in that way it is a little different than throws / try

@rayfix
Copy link

rayfix commented May 6, 2017

Feel free to use anything in my fork. You don't need to give me any credit. :]

@koher
Copy link
Author

koher commented May 7, 2017

Thank you so much about your review and correction :-) I merged your update. It is very helpful to me.

In this case, it doesn't wait because it marked the function async. But it really has to because the data dependency. So in that way it is a little different than throws / try

It does not need to wait at the line of return await try parse(data) because the getModels is marked with async. It really waits somewhere it is called and handled by do / wait.

func getModels() async throws -> [Model] {
  let data = await try download()
  return await try parse(data)  // does not need to wait here
}

let models: [Model]
do {
  models = await try getModels()
} wait // needs to wait here

// uses models here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment