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).
So, the problem is a predefined order to evaluate the values, not a
"second" await.
Like you said, the person can write the wrong code ever anyway!
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)
}
}
In this case is not better discuss the precedence order to evaluate the
values?
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.
Precedence order still be a problem using another "await" keyword.
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.
The proposal already covered this:
await DispatchQueue.main.syncCoroutine()
Maybe not the best way, but this is one possible way.
···
Em ter, 12 de set de 2017 às 16:48, Adam Kemp via swift-evolution < swift-evolution@swift.org> escreveu:
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution