await keyword "scope"

I decided to split this out to its own thread because it seems orthogonal to other issues being discussed.

When I read this line from the proposal:

await decodeImage(dataResource.get(), imageResource.get())

It’s not clear to me where the asynchronous call is. There are three function calls on that line. Which ones might actually suspend? You can’t tell by looking at it because there’s only one await keyword (the answer is all 3).

I’m not a fan of the ambiguity of applying the await keyword to an entire statement. I know some people think this is a good thing, but to me it’s just obfuscating important information.

Further, if you’re going beyond a single expression then why you would stop at the statement level? Why not make it apply to the whole block or even a whole function? Why require the keyword at all? It doesn’t appear to be adding any value if it doesn’t specify exactly where the suspension point is. “Somewhere on this line” can get rather vague.

async/await can be a huge win for clarity, but it also comes with the downside of having to think a bit more about what can happen at these suspension points. I feel like it should be a goal to make it very clear where those suspension points are so that we can more easily reason about them. That’s why I prefer restricting it to apply to a single expression. It’s very clear where the function gets suspended, which means it’s clearer where you need to be concerned about things possibly happening in between your code. Consider this, for example:

await decodeImage(loadWebResource(self.webProfilePath), loadWebResource(self.imagePath))

If webProfilePath and imagePath are vars then they could change in between those two calls. If you can’t see that these calls are suspending then you might not know to cache them up-front to ensure consistency:

let webProfilePath = self.webProfilePath
let imagePath = self.imagePath
await decodeImage(await loadWebResource(webProfilePath), await loadWebResource(imagePath))

I think a general guideline we should use when considering how this feature should work is to ask whether it makes bugs more likely or less likely. The goal should be to reduce bugs while simplifying code. If we simplify the code to the point where we’re making some bugs too subtle then we may be doing more harm than good.

"Try" does the same, but I do not know anyone who prefers to repeat the
same keyword several times.

return try func0(try func1(), try func2())

I do not think there's any value in knowing how many interim steps also
need "await" ... In practice, you have to wait for everyone anyway.

···

Em seg, 28 de ago de 2017 às 17:09, Adam Kemp via swift-evolution < swift-evolution@swift.org> escreveu:

I decided to split this out to its own thread because it seems orthogonal
to other issues being discussed.

When I read this line from the proposal:

await decodeImage(dataResource.get(), imageResource.get())

It’s not clear to me where the asynchronous call is. There are three
function calls on that line. Which ones might actually suspend? You can’t
tell by looking at it because there’s only one await keyword (the answer is
all 3).

I’m not a fan of the ambiguity of applying the await keyword to an entire
statement. I know some people think this is a good thing, but to me it’s
just obfuscating important information.

Further, if you’re going beyond a single expression then why you would
stop at the statement level? Why not make it apply to the whole block or
even a whole function? Why require the keyword at all? It doesn’t appear to
be adding any value if it doesn’t specify exactly where the suspension
point is. “Somewhere on this line” can get rather vague.

async/await can be a huge win for clarity, but it also comes with the
downside of having to think a bit more about what can happen at these
suspension points. I feel like it should be a goal to make it very clear
where those suspension points are so that we can more easily reason about
them. That’s why I prefer restricting it to apply to a single expression.
It’s very clear where the function gets suspended, which means it’s clearer
where you need to be concerned about things possibly happening in between
your code. Consider this, for example:

await decodeImage(loadWebResource(self.webProfilePath), loadWebResource(self.imagePath))

If webProfilePath and imagePath are vars then they could change in between
those two calls. If you can’t see that these calls are suspending then you
might not know to cache them up-front to ensure consistency:

let webProfilePath = self.webProfilePath
let imagePath = self.imagePath

await decodeImage(await loadWebResource(webProfilePath), await
loadWebResource(imagePath))

I think a general guideline we should use when considering how this
feature should work is to ask whether it makes bugs more likely or less
likely. The goal should be to reduce bugs while simplifying code. If we
simplify the code to the point where we’re making some bugs too subtle then
we may be doing more harm than good.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Yes, this is a reasonable concern. We debated it heavily in the Swift 2 timeframe when introducing error handling (as other’s have pointed out, it works the same way).

This is a tradeoff between the readability benefits of marking non-obvious control flow vs the readability disadvantage of having noise in the code. Requiring marking on every async or throwing call is particularly bad in the case of chaining. Compare:

   let x = try (try (try a.foo()).bar()).baz()
vs:
   let x = try a.foo().bar().baz()

In the Swift 2 timeframe, we decided that in many cases, it is mostly obvious what APIs can throw, so one marker is enough. That said, there ARE potentially confusing cases, and some programmers may want to be more explicit about marking. This is why the compiler allows you to explicitly mark subexpressions if you’d like.

I believe that this design has served the community well, and I haven’t heard of serious problems with it. I’m pretty confident that async following the same model will have similar success.

-Chris

···

On Aug 28, 2017, at 1:09 PM, Adam Kemp via swift-evolution <swift-evolution@swift.org> wrote:

I decided to split this out to its own thread because it seems orthogonal to other issues being discussed.

When I read this line from the proposal:

await decodeImage(dataResource.get(), imageResource.get())

It’s not clear to me where the asynchronous call is. There are three function calls on that line. Which ones might actually suspend? You can’t tell by looking at it because there’s only one await keyword (the answer is all 3).

I explained what the value is already. It identifies clearly in your code where the suspension points are. Each place you see “await” would mark a location where your code may yield and allow other things to happen. Those are points where state could change unexpectedly. It’s important for someone writing asynchronous code to understand where those locations are. It’s difficult to reason about your code without that knowledge.

···

On Aug 28, 2017, at 1:16 PM, Wallacy <wallacyf@gmail.com> wrote:

"Try" does the same, but I do not know anyone who prefers to repeat the same keyword several times.

return try func0(try func1(), try func2())

I do not think there's any value in knowing how many interim steps also need "await" ... In practice, you have to wait for everyone anyway.

Em seg, 28 de ago de 2017 às 17:09, Adam Kemp via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> escreveu:
I decided to split this out to its own thread because it seems orthogonal to other issues being discussed.

When I read this line from the proposal:

await decodeImage(dataResource.get(), imageResource.get())

It’s not clear to me where the asynchronous call is. There are three function calls on that line. Which ones might actually suspend? You can’t tell by looking at it because there’s only one await keyword (the answer is all 3).

I’m not a fan of the ambiguity of applying the await keyword to an entire statement. I know some people think this is a good thing, but to me it’s just obfuscating important information.

Further, if you’re going beyond a single expression then why you would stop at the statement level? Why not make it apply to the whole block or even a whole function? Why require the keyword at all? It doesn’t appear to be adding any value if it doesn’t specify exactly where the suspension point is. “Somewhere on this line” can get rather vague.

async/await can be a huge win for clarity, but it also comes with the downside of having to think a bit more about what can happen at these suspension points. I feel like it should be a goal to make it very clear where those suspension points are so that we can more easily reason about them. That’s why I prefer restricting it to apply to a single expression. It’s very clear where the function gets suspended, which means it’s clearer where you need to be concerned about things possibly happening in between your code. Consider this, for example:

await decodeImage(loadWebResource(self.webProfilePath), loadWebResource(self.imagePath))

If webProfilePath and imagePath are vars then they could change in between those two calls. If you can’t see that these calls are suspending then you might not know to cache them up-front to ensure consistency:

let webProfilePath = self.webProfilePath
let imagePath = self.imagePath
await decodeImage(await loadWebResource(webProfilePath), await loadWebResource(imagePath))

I think a general guideline we should use when considering how this feature should work is to ask whether it makes bugs more likely or less likely. The goal should be to reduce bugs while simplifying code. If we simplify the code to the point where we’re making some bugs too subtle then we may be doing more harm than good.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

I don't believe that having "await" everywhere will remind you that you have to precalculate and cache operands to suspending operations. In fact, it sounds like your goal is not to enforce that every async operation is gated behind `await`, but to ensure that developers cache the operands of asynchronous operations if they need to do it. IMO, you're choosing the wrong tool for this. `Await` goes on the operation, not on the operands; but it's the operands that you're trying to protect. This example has one `await` per asynchronous operation but it still exhibits the problem that you're hoping to solve:

let webProfile = await loadWebResource(self.webProfilePath)
let image = await loadWebResource(self.imagePath)
await decodeImage(webProfile, image)

This code doesn't feel any more wrong than your single statement with three asynchronous operations, and to me that means that your proposed solution is insufficient to address the problem that you describe (if there's a problem at all).

Additionally, in the majority of cases, `await`ing at every async expression will be just noise. For instance, `await foo(await bar())` can't possibly exhibit the problem that you describe because bar() is guaranteed to complete before foo() can start. In that case, it's just noise.

Félix

···

Le 28 août 2017 à 13:37, Adam Kemp via swift-evolution <swift-evolution@swift.org> a écrit :

I explained what the value is already. It identifies clearly in your code where the suspension points are. Each place you see “await” would mark a location where your code may yield and allow other things to happen. Those are points where state could change unexpectedly. It’s important for someone writing asynchronous code to understand where those locations are. It’s difficult to reason about your code without that knowledge.

On Aug 28, 2017, at 1:16 PM, Wallacy <wallacyf@gmail.com <mailto:wallacyf@gmail.com>> wrote:

"Try" does the same, but I do not know anyone who prefers to repeat the same keyword several times.

return try func0(try func1(), try func2())

I do not think there's any value in knowing how many interim steps also need "await" ... In practice, you have to wait for everyone anyway.

Em seg, 28 de ago de 2017 às 17:09, Adam Kemp via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> escreveu:
I decided to split this out to its own thread because it seems orthogonal to other issues being discussed.

When I read this line from the proposal:

await decodeImage(dataResource.get(), imageResource.get())

It’s not clear to me where the asynchronous call is. There are three function calls on that line. Which ones might actually suspend? You can’t tell by looking at it because there’s only one await keyword (the answer is all 3).

I’m not a fan of the ambiguity of applying the await keyword to an entire statement. I know some people think this is a good thing, but to me it’s just obfuscating important information.

Further, if you’re going beyond a single expression then why you would stop at the statement level? Why not make it apply to the whole block or even a whole function? Why require the keyword at all? It doesn’t appear to be adding any value if it doesn’t specify exactly where the suspension point is. “Somewhere on this line” can get rather vague.

async/await can be a huge win for clarity, but it also comes with the downside of having to think a bit more about what can happen at these suspension points. I feel like it should be a goal to make it very clear where those suspension points are so that we can more easily reason about them. That’s why I prefer restricting it to apply to a single expression. It’s very clear where the function gets suspended, which means it’s clearer where you need to be concerned about things possibly happening in between your code. Consider this, for example:

await decodeImage(loadWebResource(self.webProfilePath), loadWebResource(self.imagePath))

If webProfilePath and imagePath are vars then they could change in between those two calls. If you can’t see that these calls are suspending then you might not know to cache them up-front to ensure consistency:

let webProfilePath = self.webProfilePath
let imagePath = self.imagePath
await decodeImage(await loadWebResource(webProfilePath), await loadWebResource(imagePath))

I think a general guideline we should use when considering how this feature should work is to ask whether it makes bugs more likely or less likely. The goal should be to reduce bugs while simplifying code. If we simplify the code to the point where we’re making some bugs too subtle then we may be doing more harm than good.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

I'm not convinced by this example. If webProfilePath and imagePath are
mutable, then the expression you wrote is extremely difficult to reason
about even without any async/await. I fail to see how three awaits are
better than one in helping out the situation.

···

On Mon, Aug 28, 2017 at 15:37 Adam Kemp via swift-evolution < swift-evolution@swift.org> wrote:

I explained what the value is already. It identifies clearly in your code
where the suspension points are. Each place you see “await” would mark a
location where your code may yield and allow other things to happen. Those
are points where state could change unexpectedly. It’s important for
someone writing asynchronous code to understand where those locations are.
It’s difficult to reason about your code without that knowledge.

On Aug 28, 2017, at 1:16 PM, Wallacy <wallacyf@gmail.com> wrote:

"Try" does the same, but I do not know anyone who prefers to repeat the
same keyword several times.

return try func0(try func1(), try func2())

I do not think there's any value in knowing how many interim steps also
need "await" ... In practice, you have to wait for everyone anyway.

Em seg, 28 de ago de 2017 às 17:09, Adam Kemp via swift-evolution < > swift-evolution@swift.org> escreveu:

I decided to split this out to its own thread because it seems orthogonal
to other issues being discussed.

When I read this line from the proposal:

await decodeImage(dataResource.get(), imageResource.get())

It’s not clear to me where the asynchronous call is. There are three
function calls on that line. Which ones might actually suspend? You can’t
tell by looking at it because there’s only one await keyword (the answer is
all 3).

I’m not a fan of the ambiguity of applying the await keyword to an entire
statement. I know some people think this is a good thing, but to me it’s
just obfuscating important information.

Further, if you’re going beyond a single expression then why you would
stop at the statement level? Why not make it apply to the whole block or
even a whole function? Why require the keyword at all? It doesn’t appear to
be adding any value if it doesn’t specify exactly where the suspension
point is. “Somewhere on this line” can get rather vague.

async/await can be a huge win for clarity, but it also comes with the
downside of having to think a bit more about what can happen at these
suspension points. I feel like it should be a goal to make it very clear
where those suspension points are so that we can more easily reason about
them. That’s why I prefer restricting it to apply to a single expression.
It’s very clear where the function gets suspended, which means it’s clearer
where you need to be concerned about things possibly happening in between
your code. Consider this, for example:

await decodeImage(loadWebResource(self.webProfilePath), loadWebResource(self.imagePath))

If webProfilePath and imagePath are vars then they could change in
between those two calls. If you can’t see that these calls are suspending
then you might not know to cache them up-front to ensure consistency:

let webProfilePath = self.webProfilePath
let imagePath = self.imagePath

await decodeImage(await loadWebResource(webProfilePath), await
loadWebResource(imagePath))

I think a general guideline we should use when considering how this
feature should work is to ask whether it makes bugs more likely or less
likely. The goal should be to reduce bugs while simplifying code. If we
simplify the code to the point where we’re making some bugs too subtle then
we may be doing more harm than good.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

I explained what the value is already. It identifies clearly in your code where the suspension points are. Each place you see “await” would mark a location where your code may yield and allow other things to happen. Those are points where state could change unexpectedly. It’s important for someone writing asynchronous code to understand where those locations are. It’s difficult to reason about your code without that knowledge.

I understand the value you perceive but I think that the try and await solution of applying to the whole statement is a good compromise to greatly improve readability at the expense of a little lost information.

···

On 28 Aug 2017, at 22:37, Adam Kemp via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 28, 2017, at 1:16 PM, Wallacy <wallacyf@gmail.com> wrote:

"Try" does the same, but I do not know anyone who prefers to repeat the same keyword several times.

return try func0(try func1(), try func2())

I do not think there's any value in knowing how many interim steps also need "await" ... In practice, you have to wait for everyone anyway.

Em seg, 28 de ago de 2017 às 17:09, Adam Kemp via swift-evolution <swift-evolution@swift.org> escreveu:
I decided to split this out to its own thread because it seems orthogonal to other issues being discussed.

When I read this line from the proposal:

await decodeImage(dataResource.get(), imageResource.get())

It’s not clear to me where the asynchronous call is. There are three function calls on that line. Which ones might actually suspend? You can’t tell by looking at it because there’s only one await keyword (the answer is all 3).

I’m not a fan of the ambiguity of applying the await keyword to an entire statement. I know some people think this is a good thing, but to me it’s just obfuscating important information.

Further, if you’re going beyond a single expression then why you would stop at the statement level? Why not make it apply to the whole block or even a whole function? Why require the keyword at all? It doesn’t appear to be adding any value if it doesn’t specify exactly where the suspension point is. “Somewhere on this line” can get rather vague.

async/await can be a huge win for clarity, but it also comes with the downside of having to think a bit more about what can happen at these suspension points. I feel like it should be a goal to make it very clear where those suspension points are so that we can more easily reason about them. That’s why I prefer restricting it to apply to a single expression. It’s very clear where the function gets suspended, which means it’s clearer where you need to be concerned about things possibly happening in between your code. Consider this, for example:

await decodeImage(loadWebResource(self.webProfilePath), loadWebResource(self.imagePath))

If webProfilePath and imagePath are vars then they could change in between those two calls. If you can’t see that these calls are suspending then you might not know to cache them up-front to ensure consistency:

let webProfilePath = self.webProfilePath
let imagePath = self.imagePath
await decodeImage(await loadWebResource(webProfilePath), await loadWebResource(imagePath))

I think a general guideline we should use when considering how this feature should work is to ask whether it makes bugs more likely or less likely. The goal should be to reduce bugs while simplifying code. If we simplify the code to the point where we’re making some bugs too subtle then we may be doing more harm than good.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

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.

That’s why I think that it is important for every one of those locations to be explicitly marked so that it is very clear where the breaks in the function are, and thus where other code can run.

···

On Aug 28, 2017, at 11:19 PM, Chris Lattner <clattner@nondot.org> wrote:

On Aug 28, 2017, at 1:09 PM, Adam Kemp via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I decided to split this out to its own thread because it seems orthogonal to other issues being discussed.

When I read this line from the proposal:

await decodeImage(dataResource.get(), imageResource.get())

It’s not clear to me where the asynchronous call is. There are three function calls on that line. Which ones might actually suspend? You can’t tell by looking at it because there’s only one await keyword (the answer is all 3).

Yes, this is a reasonable concern. We debated it heavily in the Swift 2 timeframe when introducing error handling (as other’s have pointed out, it works the same way).

This is a tradeoff between the readability benefits of marking non-obvious control flow vs the readability disadvantage of having noise in the code. Requiring marking on every async or throwing call is particularly bad in the case of chaining. Compare:

   let x = try (try (try a.foo()).bar()).baz()
vs:
   let x = try a.foo().bar().baz()

In the Swift 2 timeframe, we decided that in many cases, it is mostly obvious what APIs can throw, so one marker is enough. That said, there ARE potentially confusing cases, and some programmers may want to be more explicit about marking. This is why the compiler allows you to explicitly mark subexpressions if you’d like.

I believe that this design has served the community well, and I haven’t heard of serious problems with it. I’m pretty confident that async following the same model will have similar success.

-Chris

What sort of bugs? Can you please provide a concrete example we can discuss?

-Chris

···

On Sep 11, 2017, at 1:42 PM, Adam Kemp <adam.kemp@apple.com> wrote:

Yes, this is a reasonable concern. We debated it heavily in the Swift 2 timeframe when introducing error handling (as other’s have pointed out, it works the same way).

This is a tradeoff between the readability benefits of marking non-obvious control flow vs the readability disadvantage of having noise in the code. Requiring marking on every async or throwing call is particularly bad in the case of chaining. Compare:

   let x = try (try (try a.foo()).bar()).baz()
vs:
   let x = try a.foo().bar().baz()

In the Swift 2 timeframe, we decided that in many cases, it is mostly obvious what APIs can throw, so one marker is enough. That said, there ARE potentially confusing cases, and some programmers may want to be more explicit about marking. This is why the compiler allows you to explicitly mark subexpressions if you’d like.

I believe that this design has served the community well, and I haven’t heard of serious problems with it. I’m pretty confident that async following the same model will have similar success.

-Chris

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.

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.

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

Would it be possible to actually fix this? That is, make the code covered by the `await` evaluate synchronous subexpressions first, such that the code sample above is equivalent to this?

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

···

On Sep 12, 2017, at 12:48 PM, Adam Kemp via swift-evolution <swift-evolution@swift.org> wrote:

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

--
Brent Royal-Gordon
Architechies

So, the problem is a predefined order to evaluate the values, not a "second" await.

No, the order of execution is well-defined. The problem isn’t in which order the arguments are executed. The problem is when that evaluation happens with respect to when we return to the run loop. That’s an entirely new problem with async/await. In Swift today it is not possible to write code that returns to the run loop in between evaluation of arguments.

Like you said, the person can write the wrong code ever anyway!

True. There are many ways to write bugs. The question is whether the language makes those bugs easier to spot or does it obfuscate them? Making await explicit every time makes this bug easier to see. If I were doing a code review I could spot that bug. Without the explicit await it would be much harder to spot by just reading the code. It’s not clear whether it’s even a bug unless you already know that “processImage” is an async function.

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()

As I’ve mentioned before, the proposal is a proposal. This is the discussion of the proposal. I think it’s reasonable for us to disagree with the proposal, especially if we can explain why we think it should change.

As someone who has used C#’s async/await feature (for iOS apps, even) I know first-hand how useful this feature can be (as well as its pitfalls). I’m excited about the prospect of using it in Swift. That’s why I want it to be as good as it can be. As proposed I don’t think it is as good as it can be. That’s why I’m participating in this discussion.

···

On Sep 12, 2017, at 1:00 PM, Wallacy <wallacyf@gmail.com> wrote:

"No, the order of execution is well-defined. The problem isn’t in which
order the arguments are executed. The problem is when that evaluation
happens with respect to when we return to the run loop. That’s an entirely
new problem with async/await. In Swift today it is not possible to write
code that returns to the run loop in between evaluation of arguments."

I understand this, however if you reverse the evaluate order, in this
particular problem, there's no bug.

My point is: When order is important, it will not be a new "await" inline
that will solve. The developer will look at the "await" and may still not
realize that the root of the bug was because `resizeSwitchIsOn` was
obtained before. Another `await` inline will not make clear this problem,
maybe will for you but not for everybody.

Actually, I think several await on the same line will only make the
debugging process more difficult.

There is no proof that this will help the developer for cases like that.
Most likely it will put the break point inside processImage and see what
values were loaded. And even at call side the debugger will tell the
expected values.

"True. There are many ways to write bugs. The question is whether the
language makes those bugs easier to spot or does it obfuscate them? Making
await explicit every time makes this bug easier to see. If I were doing a
code review I could spot that bug. Without the explicit await it would be
much harder to spot by just reading the code. It’s not clear whether it’s
even a bug unless you already know that “processImage” is an async function.
"

I really do not know if this is a really common problem. In practice I
believe that anyone who is observing the code will know, unless it is the
first time that he is doing maintenance. And if this is the case, another
await probably will not help too.

A simple break point in this line will help!

"As I’ve mentioned before, the proposal is a proposal. This is the
discussion of the proposal. I think it’s reasonable for us to disagree with
the proposal, especially if we can explain why we think it should change."

You only pointed the need to returning to the right queue. No disagreement
here. You can suggest other way of course. I also don't like this style too.

···

Em ter, 12 de set de 2017 às 17:53, Adam Kemp <adam.kemp@apple.com> escreveu:

On Sep 12, 2017, at 1:00 PM, Wallacy <wallacyf@gmail.com> wrote:

So, the problem is a predefined order to evaluate the values, not a
"second" await.

No, the order of execution is well-defined. The problem isn’t in which
order the arguments are executed. The problem is when that evaluation
happens with respect to when we return to the run loop. That’s an entirely
new problem with async/await. In Swift today it is not possible to write
code that returns to the run loop in between evaluation of arguments.

Like you said, the person can write the wrong code ever anyway!

True. There are many ways to write bugs. The question is whether the
language makes those bugs easier to spot or does it obfuscate them? Making
await explicit every time makes this bug easier to see. If I were doing a
code review I could spot that bug. Without the explicit await it would be
much harder to spot by just reading the code. It’s not clear whether it’s
even a bug unless you already know that “processImage” is an async function.

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()

As I’ve mentioned before, the proposal is a proposal. This is the
discussion of the proposal. I think it’s reasonable for us to disagree with
the proposal, especially if we can explain why we think it should change.

As someone who has used C#’s async/await feature (for iOS apps, even) I
know first-hand how useful this feature can be (as well as its pitfalls).
I’m excited about the prospect of using it in Swift. That’s why I want it
to be as good as it can be. As proposed I don’t think it is as good as it
can be. That’s why I’m participating in this discussion.

That violates the defined order of evaluation for function arguments. You could also write code in which the (async) first argument function call has side effects that alter the result of the second argument expression. I’m not saying that’s good code, but it’s possible, and the language defines the order of evaluation so that code like that will have a predictable behavior.

···

On Sep 15, 2017, at 4:50 AM, Brent Royal-Gordon <brent@architechies.com> wrote:

On Sep 12, 2017, at 12:48 PM, Adam Kemp via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

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

Would it be possible to actually fix this? That is, make the code covered by the `await` evaluate synchronous subexpressions first, such that the code sample above is equivalent to this?

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

"No, the order of execution is well-defined. The problem isn’t in which order the arguments are executed. The problem is when that evaluation happens with respect to when we return to the run loop. That’s an entirely new problem with async/await. In Swift today it is not possible to write code that returns to the run loop in between evaluation of arguments."

I understand this, however if you reverse the evaluate order, in this particular problem, there's no bug.

I’m not sure I understand what you mean by this. We can’t reverse the evaluation order. It’s defined by the language.

Maybe you meant if the arguments themselves were ordered the other way. That would “fix” the bug, but the point of the example isn’t “show me how to fix this bug”. The point is that “given only this one line of code you can’t tell me whether the switch’s property is read before returning to the run loop or after”. There’s not enough information in that line of code itself to determine whether that’s a bug.

On the other hand, with an extra “await” I could tell you without looking at any other code that there is a bug on that line. It’s a difference in clarity.

Will it prevent all bugs like this from happening? Of course not. That’s not the point. The point is that by making the code clearer we can make it actually possible to look at the code and reason about it in isolation. We still have to understand the idea that the switch should be read before returning to the run loop, and we still have to actually write the code such that it does that. But the added clarity of an explicit await gives us enough information to know where that read needs to be. That’s valuable not only to the person writing the code, but to someone reviewing the code as well.

Actually, I think several await on the same line will only make the debugging process more difficult.

How so? It would literally compile to the same code so I’m not sure how the debugger would be affected.

I really do not know if this is a really common problem. In practice I believe that anyone who is observing the code will know, unless it is the first time that he is doing maintenance. And if this is the case, another await probably will not help too.

Again, as someone who has actually used async/await in practice in shipping code I am telling you that this is a common problem, and it does benefit someone reading that code. This isn’t speculation. This is experience.

A simple break point in this line will help!

I shouldn’t have to run code in a debugger to be able to spot this bug.

···

On Sep 12, 2017, at 2:40 PM, Wallacy <wallacyf@gmail.com> wrote:

Totally agree with Adam.

"No, the order of execution is well-defined. The problem isn’t in which order the arguments are executed. The problem is when that evaluation happens with respect to when we return to the run loop. That’s an entirely new problem with async/await. In Swift today it is not possible to write code that returns to the run loop in between evaluation of arguments."

I understand this, however if you reverse the evaluate order, in this particular problem, there's no bug.

I’m not sure I understand what you mean by this. We can’t reverse the evaluation order. It’s defined by the language.

Maybe you meant if the arguments themselves were ordered the other way. That would “fix” the bug, but the point of the example isn’t “show me how to fix this bug”. The point is that “given only this one line of code you can’t tell me whether the switch’s property is read before returning to the run loop or after”. There’s not enough information in that line of code itself to determine whether that’s a bug.

On the other hand, with an extra “await” I could tell you without looking at any other code that there is a bug on that line. It’s a difference in clarity.

I, too, think that this is important (and different from the case with `try`).

Will it prevent all bugs like this from happening? Of course not. That’s not the point. The point is that by making the code clearer we can make it actually possible to look at the code and reason about it in isolation. We still have to understand the idea that the switch should be read before returning to the run loop, and we still have to actually write the code such that it does that. But the added clarity of an explicit await gives us enough information to know where that read needs to be. That’s valuable not only to the person writing the code, but to someone reviewing the code as well.

Actually, I think several await on the same line will only make the debugging process more difficult.

How so? It would literally compile to the same code so I’m not sure how the debugger would be affected.

I really do not know if this is a really common problem. In practice I believe that anyone who is observing the code will know, unless it is the first time that he is doing maintenance. And if this is the case, another await probably will not help too.

Again, as someone who has actually used async/await in practice in shipping code I am telling you that this is a common problem, and it does benefit someone reading that code. This isn’t speculation. This is experience.

A simple break point in this line will help!

I shouldn’t have to run code in a debugger to be able to spot this bug.

Totally agree with that, especially in a language like Swift with a strong type system.

-Thorsten

···

Am 13.09.2017 um 00:39 schrieb Adam Kemp via swift-evolution <swift-evolution@swift.org>:

On Sep 12, 2017, at 2:40 PM, Wallacy <wallacyf@gmail.com> wrote:

Would it be possible to actually fix this? That is, make the code covered by the `await` evaluate synchronous subexpressions first, such that the code sample above is equivalent to this?

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

That violates the defined order of evaluation for function arguments.

I understand that, but this order of evaluation was designed before we had `await`. I'm suggesting that, now that we do, we should change the order so that synchronous subexpressions are evaluated before asynchronous ones.

You could also write code in which the (async) first argument function call has side effects that alter the result of the second argument expression. I’m not saying that’s good code, but it’s possible, and the language defines the order of evaluation so that code like that will have a predictable behavior.

You could write something like that, but as you say, that's not necessarily good code. Honestly, I don't think you can write good code that depends on one part of an expression being evaluated after an async call in another part of the expression completes.

···

On Sep 18, 2017, at 9:47 AM, Adam Kemp <adam_kemp@apple.com> wrote:

--
Brent Royal-Gordon
Architechies