>> 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.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to