I can’t speak to the more low-level implications, but to the extent this is essentially “syntactic sugar for completion handlers,” I can base my opinion on making iOS apps in different contexts for about 5 years. (Long enough to remember when Objective-C Blocks as completion handlers were the great new thing that was going to perfectly solve all our problems). I especially appreciated the clear goals section of the proposal, which I thought was on-target.
I have some uncertainty about how async/await will work in practice—literally, how to use it. Some of this is just needing to see many more examples and examples in common scenarios at the site of use and in the creation of async functions. Most of the questions I had I answered myself in the course of writing this up, but I include them for confirmation and to call attention to aspects that may not be clear. (I also tend to use block and closure interchangeably when it reads better, especially where I learned them as objective-C completion blocks, please do correct if necessary.) I hope the write up will provide some clarity for others (assuming it's basically correct or correctly labeled where it may not be) and provide feedback on where it could be more clear. *Starting at beginAsync* @IBAction func buttonDidClick(sender:AnyObject) { // 1 beginAsync { // 2 let image = await processImage() imageView.image = image } // 3 } After entering a beginAsync block, code will continue to execute inside the block (like do blocks now) until it encounters an await-marked function. In this sense it’s more beginAsyncContext not begin-being-asynchronous, which is how it could be read. Although the end of this block will be where synchronicity will resume once execution goes async (suspends?) inside it. At the point of an await function, two things can happen: 1) If the function needs time to execute -- if "it suspends" is how to describe it? -- code execution will jump to outside the beginAsync block. 2) If the function has its result already, code execution will continue inside the block, or jump to catch error if it exists? (This would not be different from now in that a receiver of a completion block can invoke it immediately, before returning). This is important to clarify because after the async block (or any functions with completion blocks now) code afterwards can’t assume what might have happened. But this proposal doesn’t solve the problem seen in the @IBAction example, of order 1, 3, 2, which is actually worse because if you can have immediate completion you aren’t sure if it is 1, 3, 2, or 1, 2, 3. This is actually an easy enough situation to handle if you’re aware of it. *Use of suspendAsync* suspendAsync is the point at which the waiting is actually triggered? For a while this threw me at first (no pun intended). Since it looks like the more common transitive verb form of “suspend”, I read this as “suspending-the-async”, therefore, resuming. The primitives looked like beginAsync started doing something asynchronous and suspendAsync resumed synchronous execution? Maybe the better order would be asyncSuspend (paired with asyncBegin--or even better for that, asyncContext) would be less likely to be confused this way? (More on the primitives just below) However, even inside the block passed to suspendAsync the code is not asynchronous yet. The code in the block passed to suspendAsync is still executed at the time the block is passed in. That code is responsible for taking the continuation block and error block, escaping with them and storing them somewhere, and invoking either when the value or error is ready. Is it correct that those blocks will be called on thread that suspendAsync was called on? It was also somewhat unclear what happens then the block passed to suspendAsync reaches the end. Since suspendAsync is itself an async function called with await, it looks like control now passes back to the end of the original beginAsync block, wherever that is. That the getStuff() async wrapper example returns the result of the call to suspendAsync in one line obscured what was going on. That was func getStuff() async -> Stuff { return await suspendAsync { continuation in getStuff(completion: continuation) } } What's going on would be more clear over two lines. For example if we wanted to do further processing after getting our async result before returning: func getStuff() async -> Stuff { let rawStuff = await suspendAsync { continuation in getStuff(completion: continuation) } return processed(rawStuff) } Where exactly execution is paused and will resume is more clear. In fact, the show the full async/await life cycle, it’s possible to demonstrate in the same scope before introducing the semantics of async functions: beginAsync { do { let stuff = try await suspendAsync { continuation, error in //perform long-running task on other queue then call continuation, error as appropriate } //Continuation block resumes here with `stuff` doSomething(with: stuff) } catch { //error block resumes here handleGettingStuffError(error) } } This is correct? While as the comments state it may be true that eventually many users won’t need to interact with suspendAsync (though I think beginAsync will remain common, such as the @IBAction example) it’s familiar the key method that breaks familiar procedural execution and creates the blocks that will allow it to resume. During the transition it will be especially important for those adapting their own or others’ code. It should be prominent. One opinion point that I do want to mention though about the last example: There should probably be just one suspendAsync method, the one with an error continuation. First, if there's the no-error version then that will probably proliferate in examples (seen already) coders will get learn how to use await/async ignoring errors, a habit we’ll have to break later. But if there's only one suspend method, then await would always include try, forcing programmers who want to ignore them to have an empty catch block (or use the space for a comment with your legitimate reason!). An empty/comment only catch block would also be the case for legacy APIs with no completion error parameter, though maybe these could be imported as throwing Error.nil or Error.false, etc. So all uses would look like beginAsync { … } catch { … }. beginAsync { let stuff = await suspendAsync { continuation, error in //perform long-running task on other queue then call continuation, error as appropriate } //Continuation block resumes here with `stuff` doSomething(with: stuff) } catch { //error block resumes here handleGettingStuffError(error) } I don’t lightly suggest trying to use syntax to force good habits, but in this case it would be the cleaner API. The alternative is forcing code that does handle errors to look like the last example above, obviously more ungainly than code that ignores errors. Handling errors is already too easy to ignore. There are more substantive points that I want to touch on later, largely from my use of a promise/future frameworks as part of a production app that went really well, even when working with UIKit, AppDelegate, NSNotification. (I also worked on a project where the last guy had rolled his own promises/futures system; you can guess how that went.) But I wanted to clarify the basics of use. Thanks for this proposal and everyone’s comments. Mike Sanderson On Fri, Aug 18, 2017 at 5:09 PM, Adam Kemp via swift-evolution < swift-evolution@swift.org> wrote: > Thanks for the quick response! > > On Aug 18, 2017, at 1:15 PM, Chris Lattner <clatt...@nondot.org> wrote: > > On Aug 18, 2017, at 12:34 PM, Adam Kemp <adam.k...@apple.com> wrote: > > For instance, say you’re handling a button click, and you need to do a > network request and then update the UI. In C# (using Xamarin.iOS as an > example) you might write some code like this: > > private async void HandleButtonClick(object sender, EventArgs e) { > var results = await GetStuffFromNetwork(); > UpdateUI(results); > } > > > This event handler is called on the UI thread, and the UpdateUI call must > be done on the UI thread. The way async/await works in C# (by default) is > that when your continuation is called it will be on the same > synchronization context you started with. That means if you started on the > UI thread you will resume on the UI thread. If you started on some thread > pool then you will resume on that same thread pool. > > > I completely agree, I would love to see this because it is the most easy > to reason about, and is implied by the syntax. I consider this to be a > follow-on to the basic async/await proposal - part of the Objective-C > importer work, as described here: > https://gist.github.com/lattner/429b9070918248274f25b714dcfc76 > 19#fix-queue-hopping-objective-c-completion-handlers > > > Maybe I’m still missing something, but how does this help when you are > interacting only with Swift code? If I were to write an asynchronous method > in Swift then how could I do the same thing that you propose that the > Objective-C importer do? That is, how do I write my function such that it > calls back on the same queue? > > In my mind, if that requires any extra effort then it is already more > error prone than what C# does. > > > Another difference between the C# implementation and this proposal is the > lack of futures. While I think it’s fair to be cautious about tying this > proposal to any specific futures implementation or design, I feel like the > value of tying it to some concept of futures was somewhat overlooked. For > instance, in C# you could write a method with this signature: > > ... > > > The benefit of connecting the async/await feature to the concept of > futures is that you can mix and match this code freely. The current > proposal doesn’t seem to allow this. > > > The current proposal provides an underlying mechanism that you can build > futures on, and gives an example. As shown, the experience using that > futures API would work quite naturally and fit into Swift IMO. > > > I feel like this is trading conceptual complexity in order to gain > compiler simplicity. What I mean by that is that the feature feels harder > to understand, and the benefit seems to be that this feature can be used > more generally for other things. I’m not sure that’s a good tradeoff. > > The other approach, which is to build a specific async/await feature using > compiler transformations, may be less generic (yield return would have to > work differently), but it seems (to me) easier to understand how to use. > > For instance, this code (modified from the proposal): > > @IBAction func buttonDidClick(sender:AnyObject) { > doSomethingOnMainThread(); > beginAsync { > let image = await processImage() > imageView.image = image > } > doSomethingElseOnMainThread(); > } > > > Is less straightforward than this: > > @IBAction async func buttonDidClick(sender:AnyObject) { > doSomethingOnMainThread(); > let imageTask = processImage() > doSomethingElseOnMainThread(); > imageView.image = await imageTask > } > > > It’s clearer from reading of the second function what order things will > run in. The code from the proposal has a block of code (the callback from > beginAsync) that will run in part before the code that follows, but some of > it will run after buttonDidClick returns. That’s confusing in the same way > that callbacks in general are confusing. The way that async/await makes > code clearer is by making it more WYSIWYG: the order you see the code > written in is the order in which that code is run. The awaits just mark > breaks. > > _______________________________________________ > 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