4 ms·
If you squint closely enough, you can recognize features of the "async/await" discussion in other longstanding issues such as the "effectless/effectful" distinc
by kannanvijayan 5y ago
If you squint closely enough, you can recognize features of the "async/await" discussion in other longstanding issues such as the "effectless/effectful" distinction and whether that should be brought up in syntax, or the "continuations/finite-exits" control transfer distinction (finite-exits being traditional languages which provide a fixed set of control trasnfer such as return/throw/break).
The "all things are (posslby) async" is a language design choice similar to "all things are (possibly) effectful" for a procedural language. If you keep your language semantics in the less generalized domain (e.g. functional vs. effectful), you have to invent syntax to talk about the other semantics easily.
So you come up with monad systems in functional languages to talk about effects, and make some nice syntax for that. And you have your effectful functions and effectless functions segregated by type signatures that flow all the way up to the top.
And you come up with async/await systems in traditional sync-semantics languages, and provide syntax to help with that - whether Javascript or Rust. In both Rust and JS, async functions are distinguished purely in the type-system, with some helpful syntax around it. In both, they desugar to `func(...): Promise<T>` or `func(...) -> impl Future<T>`. Those types too, flow "all the way to the top", imposing a colouring on code.
Erlang solved this problem by adopting the general semantics for erlang to achieve erlang's goals, and it seems that the "everything is async" semantics works there (I agree that it's a model that deserves deeper exploration).
Languages like Rust can't solve this problem the way erlang solves it, because it takes away from other fundamental aspects of the language. It would necessitate the inclusion of a very specific and very heavy (relative to Rust's expectations) runtime, along with lots of changes to the language that would diminish it along other dimensions.
- whizzter 5y agoInteresting analogy, seems like many of these binary choices cause divides. My point about the design being mistake is the way you need to be explicit in invoking with await and the often harmful side-effect of forgetting the await expression (that in practice becomes a code-path fork that can become tricky to debug if the awaited function is a void function with no unexpected values coming out of the missed await expression). For JavaScript the dynamic type-system really leaves no choice since the caller has no information about the behaviour of the callee, so await for these dynamic languages is kinda unavoidable (unless the entire language is green-threaded thatis). For Rust and C# (and probably others) the compiler knows about this in advance so the "await" expression just adds an extra layer of work for the developer (that is also error prone). There is little reason that for example the C# compiler couldn't have auto-inserted the await for all Task<> returning functions inside every (async) marked function and instead had a (direct) operator for the FEW cases where a developer really wants to handle the Task<> object manually instead of just awaiting it (C# is quite pragmatic in many other cases so the little loss of direct developer control would have been far outweighted by better symmetry and less ceremony). Practical? Definitely, the Zig language seems to even handle this entirely under the hood by it's compile-time functionality. So for a developer the red-blue distinction is mostly something handler by the compiler despite not affecting the runtime more than some extra code i guess?
- kannanvijayan 5y agoAfter some thought, I think I basically agree with that main point. I've actually been dipping my feet heavily into async programming in rust (personal stuff), and async programming in typescript (work stuff) after about a decade mostly doing C++ stuff (previous work). The challenges and confusions you mention are pretty fresh in my mind. Maybe switching the default semantic behaviour over to recognize the async type signatures and implementing this sort of default syntax transform is the better way to go. I wonder if it might cause problems with generic code - where a return type for some otherwise non-async function happens to "become a future" due to type-specialization, turning the function async? But maybe that's not an issue because the call-specialization behaviour just transitively applies to all the callers up the chain, same as any other specialization. Beyond that, I think this whole conversation has a hard time moving forward before we get a good industry understanding of high-level concurrency structures in practice. So far the major one people fixate on is "actors", which usually reduces to "stream processor".. which is a nice construct but by itself not enough to build complete programs out of. You need an entire structural framework to talk about what produces the things the stream processes. Sometimes that'll be other streams, sometimes that'll be static, sometimes that'll be some collection of random event sources, some of which are direct and some of which are indirect. Then you need ways of talking about how those producers relate to each other, and _some_ way of identifying and preserving global / regional / local invariants. Error handling is a nightmare. I haven't yet found any good vocabulary to talk about shutdown order for tasks and error handling in, how to express it clearly in the program intent, and how to make sure that intent doesn't rot over time. It's all ad-hoc promises and hooks and callbacks wiring up _this_ event to _that_ thing.. accompanied by small or large comments along with the hope that your fellow developer is cleverer than you and can actually figure out the intent from just the text of the code. After we get a few good examples of those sorts of things, it'll be easier to go back and discuss syntax and semantics, and we'll have a reference set of designs/patterns to check against and make sure we can express cleanly.
- singularity2001 5y agoThanks for the deep helpful analysis