Side note: The extensible syntax thread is actually not about async-await, async-await was just used as an example. Probably not the best move I ever made, the conversation I actually wanted to have never really materialized there. I'm not sure what to do with that now, maybe I should do an extra thread on algebraic effects? Or rename the above one another time to make the topic even clearer?
I like the async/await syntax generally being proposed, modeled off of other languages, but don't find it to be as much of a developer pain point as the amount and the timeline of discussion here suggests. It just seems like a bit of sugar over a simple Future/Promise type that wraps a closure which you can write in 30 lines of Swift.
It’s true that there’s not a whole lot you can do with async/await that you can’t do right now with closures. However, as soon as you have even moderately complex data dependencies, closures can become very unwieldy.
For example, here’s a real piece of code I wrote about a year ago using futures with Vapor.
let session = try cookie.flatMap(try req.make(Sessions.self).readSession) ?? req.future(nil)
_ = session.unwrap(or: Abort(.unauthorized)) // verify we have a session
.then { req.authorization($0) }.unwrap(or: Abort(.unauthorized))
.thenThrowing { (auth: Authorization) -> Authorization in // and view authorization
guard auth.permissions == .view else { throw Abort(.unauthorized) }
return auth
}.and(req.databaseConnection(to: .questions))
.then { arg in Question.query(on: arg.1).filter(\.groupID == arg.0.group.id!).all().map { (arg.0, $0) } } // fetch the questions
.map {
let (auth, questions) = $0
// ..
}
To be fair, it’s not the most well-written code, but that’s because it’s something I hacked together in an afternoon for a short-lived project I’ll never touch again. It should have taken me 30 seconds to write a couple database and session lookups with async/await, but instead I had to fight with nested closures for 20 minutes (with all of the slow compile times and unhelpful error messages involved) until I finally got to something that worked.
With async/await, I could have just done this:
let futureConnection = req.databaseConnection(to: .questions)
// verify we have a session and view authorization
guard
let cookie = cookie,
let session = await req.make(Sessions.self).readSession(cookie),
let authorization = await req.authorization(session),
authorization.permissions == .view else {
throw Abort(.unauthorized)
}
// fetch the questions
let questions = await Question.query(on: futureConnection).filter(\.groupID == auth.group.id!).all()
// ...
Note how much easier that is both to write and to read. The program structure naturally matches the dataflow, and normal language feature like guard
, throw
, and local variables just work — even in an asynchronous context.
Sorry for the delayed response (I have been in the middle of moving).
It could be naively/quickly implemented as a wrapper around some sort of future, but it doesn't have to be, and I think that there are other options which are better. My point above was that when the function is called normally (aka implicitly awaited), it is essentially equivalent to async/await from other languages (and can be implemented in the same way), even if it is spelled slightly differently.
The main points are:
- The actual implementation is undefined and can be changed behind the scenes (and different optimizations can be used in different circumstances).
- This spelling allows more natural progressive disclosure (e.g. async functions behave like normal functions when called normally... using async and await unlocks additional functionality/behavior)
- This spelling also allows more functionality than traditional async/await (e.g. future-like values)
- Normal Swift control-flow structures can be used
But of course.
Still, the specification can't change behind the scene, and different specifications differ in flexibility. A coroutine-based implementation would have hard time returning incomplete result without some level of Future
object. So when I saw async Int
and coroutine in the same sentence, it got me intrigued. If it's just a future with state machine, well, so be it, I figured as much. It is still a valid design though.
In any case, I'm not sure having async Value
around is a good idea. Especially if we're concerned about execution context.
Not much of an improvement really, now you just have asynchronous calls littered in your control flow instead of wrapped nicely in closures.
There seems to be a pretty fundamental disconnect here so I think it would be good to talk it through. Why do you think having async calls "littered" throughout the code is a bad thing? And why is having a series of (possibly nested) closures a good thing? What are the pros and cons of the two approaches in your mind? Do you actually find the closures version easier to write, understand, and maintain? If so then why do you think that is true?
From my point of view there's no
async let x: Int
let x: async Int
Let's use common sense here, the behavior is just as throws with try.
let x: Int = await someFunc()
And that's it, I do not think this needs any more language shanenigans to create the semantics.
Another topic is what we've how is something considered asynchronous. We have several methods: DispatchWorkItem, DispatchQueue, semaphores, mutex, spinlock, etc, that are used today to create concurrency models. I think this is another big topic that's left behind in the discussion.
Also I think we have to take a look at Kotlin concurrency model used for corroutines and learn from the path they've already walked.
I agree with @Chris_Lattner3 here and would like to reiterate the importance of cancelation. Libdill is another amazing concurrency library which introduces the concept of structured concurrency. Venice is a library that wraps libdill and provides the same mechanisms in Swift. One thing that it misses is a way to mark functions that can yield, so that the user always knows which paths the execution might take. 5 years ago, before swift was open sourced, I proposed "yielding functions". Nowadays I see that we can do much more than "yielding functions", like the actor model @Chris_Lattner3 proposed, for example. I believe "structured concurrency" is extremely powerful and, although I haven't thought through enough to assess if it fits preemptive multitasking, I know from experience (in Swift) that cooperative multitasking is valuable enough to have it be supported by the language itself. Many of this concepts are, as @Chris_Lattner3 said, orthogonal, but we should strive to make them compose well together. Since @John_McCall expressed his concerns about the scope of this thread I would like to ask if I should continue the discussion on orthogonal topics here or if I should create other threads and link this and other threads that might be relevant instead. The important thing is that the solutions we come up should compose well. Achieving this without intersections between the proposals would be close to impossible. It's hard to connect all the dots without looking at the big picture.
I've been working on a comprehensive design for concurrency in Swift which I hope to have ready to share in the next few weeks. That is probably the right point at which to pick up this conversation.
Related to @Douglas_Gregor s PR?
Looks amazing and a really big surprise. Well it be merged into 5.3-release, or would be part of 6+ ?
I believe it's a pretty safe bet that no (additional) major features will be landing in Swift 5.3. Even relatively minor bug fixes don't make the cut at this point.
Pease be actors!
Even though the recent developments are very exciting, let’s please not turn this in to a speculation thread. JMC already said that the details would be made available soon.
I'd just like to throw in a note of concern for the way closures quietly confer reference semantics on the values they capture. If not for this slippery hole in the language guarantees, value semantics and the law of exclusivity would be enough to support the provable thread-safety of most code. In related threads I have seen lots of discussion of actors and queues and other concurrency mediators, but I haven't seen any attention given to this issue with ordinary code they may execute. I think move-only closures may be an important part of the answer and I wonder about attacking concurrency without an ownership model that supports non-copyable types.
/cc @saeta
You should check out the latest merge commit of swift concurrency lib support at Merge pull request #33196 from DougGregor/concurrency-lib · apple/swift@e2cdc5e · GitHub
Before the official concurrency proposal is presented I wanted to quickly sketch what I had on my mind on this topic:
Why not use Combine streams as channels between entities called Reactors
? More and more APIs get a publisher
method so a concurrency proposal could utilize that.
Reactors would have input and output Ports
which could be connected by streams. Once data arrives on an input port a reactor would proceed in its control flow possibly sending data to its output ports until it comes to a point where it waits for the next reaction.
This step wise processing is supported by specialized functions called activities
, which allow to wait for the next step via the await
statement. Normal functions and methods can be called from activities, but not the other way around.
Besides the capability to await the next instant, activities also support concurrent control-flows and preemption as in other imperative synchronous languages. The causality follows the Sequentially Constructive model and thus allows memory to be used as synchronization mechanism between concurrent trails (as opposed to using signals for synchronization like in Esterel).
In addition to the port based data-interface, reactors might also offer a functional API through service
methods. These methods would also have a stream based signature (at least for the return value) and internally spawn new reactors on each invocation to do the processing. The result of these service calls will be handled by a special statement available in activities called receive
.
So, the general idea is that of GALS - locally synchronous reactors which are asynchronously connected via Combine streams.
Mkay… I did, but what am I supposed to notice about that commit that relates to my post?
That assumes that Apple will make Combine open-source, or the Core team invests resource in something like OpenCombine, at least for Linux/Windows