Can anyone come up with a reasonable explanation of how async/await, (synchronous) generators, and coroutines relate to each other on a conceptual level? Not specific to any one language
@joepie91 what is - each other?
@joepie91 async functions return a thunk (think an uncomputed value), await waits until that thunk is resolved.
Generators are just "freeze points" in functions so that you can make iterators as well as fake concurrency.
@cadey It feels like both are variants on the same mechanism, though, and various sources are implying as such, but nobody seems to actually be finishing the thought
@joepie91 They basically are yeah, but usually async functions only have one possible return point but generators can have infinite possible return points.
@cadey In the sense that async functions always return / yield control to "the event loop implementation", but generators return / yield control to whatever the call-site was?
@joepie91 bingo
@cadey Thanks, that confirms my general sense of it then, I think :)
@joepie91 like a general proper explanation for common consumption?
I think I have some understanding but wouldn't claim to be the expert. Can try a quick one.
@virtulis More a mental model / framing to reason about them, from the perspective of "implementing them in a compiler for a custom language", without ending up with a ton of duplicated complexity
@joepie91 Ah.
Well, I'll start in reverse.
A coroutine is any _code_ that executes independently of the main _call stack_ (but not necessarily in parallel, and usually not) and is started and controlled from that main call stack. Both async/await and generators can be considered coroutines, as well as CSP/Actors (not aware what the significant distinction between the two is).
The distinctinction between the types of them is then mostly about what triggers their execution and suspension/resumption.
Generators are directly started and resumed by the calling code (also passing in a value), but decide themselves when to pause or stop (also returning a value).
Async functions are started by the caller but then are controlled by the runtime itself, by being able to pass a promise to it and be resumed when the promise resolves. The caller uses the same mechanism to wait for the result/failure.
So I'd say the main difference is how the execution controlled and by what. Async functions are managed by an "executor", be it V8 or Tokio. Generators are managed by the caller. Processes in Erlang are managed by the VM but the message sending and listening are not coupled together.
I'm not aware of any implementations where the (synchronous) execution can be paused.
So I think in a language implementation all you need is a concept of a coroutine that can stop itself (with a value) and be resumed (with a value). The rest is "userspace".
Hope that was at least a bit useful :D
@joepie91 so basically what you said 20 minutes ago but 10x longer!
@virtulis It helped to solidify some of the details though, thanks :)
@joepie91 yes! but it would require a whiteboard and a beer :)
it has to do with yield/await being "continuation points" where the state of the stack needs to be preserved, which makes them have basically identical behavior to the runtime.
it's interesting to dig into the code typescript generates if you use async/await and target a JS engine that predates native async/await support
(This is less an "I don't understand it" and more an "I can't quite build up a coherent-enough mental model to write a maximally-simple implementation, and maybe different framings will help")