So, you want to add these async/~await/ keywords?
First of all, it has already been seriously researched by the C#/F# .NET guys. Just learn what they have come up with.
One’s own principle-guided reasoning could proceed like this:
The fundamental difference between ordinary procedures and async
procedures is the whole protocol for calling and returning of values,
and dealing with actual implementation of the corresponding mechanisms
(abstract at this point, but has to reuse what is already out there).
Conceptually, there are two layers - user-facing syntactic layer, which conveys an over-simplified semantics (this is the whole point - to hide the complexity behind simple syntactic forms), and an implementation layer, which defines how everything actually work and represented, including all the obsession with performance and the zero-cost abstraction memes.
These high-level considerations already imply (demand) a clear separation via non-leaking abstraction barriers, which we almost never see in “systems languages” (another meme) – with unnecessary machine and ABI level constructs being scattered everywhere.
With the async
syntactic sugar, however, we want to stay abstract
and high-level (the exactly right level of abstraction), and, again,
this is the point. Otherwise we will just add more bloat.
Lets look at the list-comprehensions of Haskell and macros of Common Lisp for inspirations. These are exactly “higher level abstractions at the level of syntax” that we want.
They are not as general as the ability to define new macros (Rust already has this) and it is not an another pre-defined macro.
They are just new syntactic forms which abstract out the underlying complexity and hide the irrelevant low-level details. Think of the special syntax to deal with hardware ports in C.
The principle is this:
We just add a new keyword in front of a procedure and nothing else has to changed within it. Especially no semantic changes whatsoever.
This is the point - all the required wrapping and whole protocol has to be hidden behind the extra keyword.
The procedure decorated with the keywords will technically be of a different kind, but the semantics of a general procedure call (in particular all the types) has to remain unchanged.
One more time, we do not explicitly wrap in a Furure
or whatever - it
all has to be implicit and abstracted out. No changes to the type
signature, as it is with decorators.
Notice that this kind of procedures is even further away from pure
functions - these may or may not be completed. This is precisely what the async
and await
keywords signifies (denotes).
The idea of modifying the type system and exposing all the implementation details is just plain wrong.
The right way is to add another kind of a syntactic closure and pretend that nothing changed (at the use side).
Imagine adding an annotation to the Lambda Calculus - we do not have to implement it in the Lambda Calculus itself. It is just at the conceptual level.
Of course, in a “system language” one would want the access to all the underlying details and actually define and implement the protocol.
This has to be done via modules (the proper way of abstraction), but with some syntactic sugar, to clearly separate it from ordinary code. .
And there the specialized “macros” (distinct from ordinary procedures) may be the proper way. These “macros” would be able to look inside of a syntactic closure and access all the boring “ThreadPollFactoryFactory” or “getThreadToWhichFutureIsBound” and what not.
The main point is - we partition the language horizontally in order to augment it with new orthogonal concepts, without messing up the type system.
We use the same universal principle of information hiding behind stable interfaces (of proper ADTs), but at the level of syntactic forms.
This is to have modularity of syntax, if you will. Similar to how
the looping macros being used in Common Lisp – how they extend the language without breaking anything.
The fact that they are defined with the defmacro
and use helpers is not the point. The point is that at the use-side they are completely abstract syntactic forms.
Just as we cannot see which particular registers and stack frames a procedure uses (this is another level of abstraction) we do not have to see thread-polls, executors and other irrelevant details, including the implementation details of Promises or Futures and whatever it is. It can even be changed if properly abstracted away.
What about errors and canceling? Well, errors has to be signalled in a standard way. Rust does not have exceptions, so it is good time to introduce them only for async code.
And we just do not cancel, just as we do not cancel endless loops.
Knowledge is power.