[swift-evolution] await keyword "scope"

Adam Kemp adam.kemp at apple.com
Tue Sep 12 14:47:34 CDT 2017


>> I think that decision makes sense for try/throws, but I feel like the await keyword is fundamentally different from that. The pitfalls of not understanding how the code is transformed and how it will behave at runtime are much greater with await than with try.
>> 
>> If you have a line of code with multiple expressions that can throw then the consequences of not knowing which particular one threw the error are minor. In most cases it doesn’t matter, and you would handle a given error the same regardless of which subexpression threw the error.
>> 
>> With await the function is actually broken up into pieces, and unrelated code can run in between those pieces on the same thread and/or the same queue. That has a much higher potential of leading to subtle bugs if you can’t see where those breaks are.
> 
> What sort of bugs?  Can you please provide a concrete example we can discuss?

Here’s just a simple example of code that looks right but is buggy:

@IBAction func buttonDidClick(sender:AnyObject) {
    beginAsync {
        let image = await processImage(downloadImage(), resize: self.resizeSwitch.isOn)
        displayImage(image)
    }
}

That code I believe would be equivalent to this more explicit code:

@IBAction func buttonDidClick(sender:AnyObject) {
    beginAsync {
        let downloadedImage = await downloadImage()
        let resizeSwitchIsOn = self.resizeSwitch.isOn
        let image = await processImage(downloadedImage, resize: resizeSwitchIsOn)
        displayImage(image)
    }
}

The subtlety here is that control can be returned to the run loop before the code checks the value of resizeSwitch.isOn. That means there is a time when the user can change the switch before it’s read.

Obviously someone could write the second version of this code and have the same bug so the problem isn’t that it’s possible to write this bug. The problem is that it’s not clear in the first example where those breaks are where control may be returned to the run loop. Someone reading the first example can’t tell when self.resizeSwitch.isOn is going to be read (now or some future iteration of the run loop).

The correct way to write this would be to read the UI up front:

@IBAction func buttonDidClick(sender:AnyObject) {
    beginAsync {
        let resizeSwitchIsOn = self.resizeSwitch.isOn
        let downloadedImage = await downloadImage()
        let image = await processImage(downloadedImage, resize: resizeSwitchIsOn)
        displayImage(image)
    }
}

Based on my experience dealing with async/await code and the subtlety of returning to the run loop in between expressions I think the added clarity of an explicit await for each call outweighs the inconvenience. It is a hard enough adjustment for people to understand that this function executes in pieces with other code running in between. I think it would be an even harder adjustment if you couldn’t use a simple rule like “every time you see await there is a break right there”. In the original example there are two breaks in the same line, and it’s opaque to the reader where those breaks are.

This example also shows once again the importance of returning to the right queue. If the “await downloadImage” continues on some other queue then you would be using UIKit on a background thread, which is not allowed. It seems like we’re starting to see some convergence on this idea, which I’m glad to see.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170912/537809da/attachment.html>


More information about the swift-evolution mailing list