The problem you're describing is two-fold.
- The program you're writing should behave asynchronously as a whole when viewed from the outside.
- It should not be visible at the call site whether a function call potentially gives up control or not.
There are a couple of ways to achieve this, but they basically boil down to
- having multiple threads (at some level of abstraction)
- having multiple kinds of function at the language level, all of which are called like this
foo(4, 7, bar, quux)
.
For (1), I'm lumping together forking and running multiple processes, spawning multiple kernel threads, and green thread implementations that schedule language-runtime level threads onto kernel threads. From the perspective of the problem, they are the same. In this world, no function ever gives up or loses control from the perspective of its thread. The thread itself sometimes doesn't have control and sometimes isn't running but you don't give up control of your own thread in this world. A system fitting this model may or may not have the ability to spawn new threads or join on existing threads. A system fitting this model may or may not have the ability to duplicate a thread like Unix's fork
.
(2) is interesting. In order to do it justice we need to talk about introduction and elimination forms.
I'm going to show why implicit await
cannot be added to a language like Javascript in a backwards-compatible way. The basic idea is that by exposing promises to the user and having a distinction between synchronous and asynchronous contexts, Javascript has leaked an implementation detail that prevents handling synchronous and asynchronous functions uniformly. There's also the fact that you can't await
a promise outside of an async function body. These design choices are incompatible with "making asynchronousness invisible to the caller".
You can introduce a synchronous function using a lambda and eliminate it with a function call.
Synchronous function introduction:
((x) => {return x + x;})
Synchronous function elimination:
f(4)
((x) => {return x + x;})(4)
You can contrast this with asynchronous function introduction and elimination.
Asynchronous function introduction
(async (x) => {return x + x;})
Asynchonrous function elimination (note: only valid inside an async
function)
await (async (x) => {return x + x;})(4)
The fundamental problem here is that an asynchronous function is also a synchronous function producing a promise object.
Here's an example of calling an asynchronous function synchronously in the node.js repl.
> (async (x) => {return x + x;})(4)
Promise { 8 }
You can hypothetically have a language, even a dynamically typed one, where the difference between asynchronous and synchronous function calls is not visible at the call site and possibly is not visible at the definition site.
Taking a language like that and lowering it to Javascript is possible, you'd just have to effectively make all functions asynchronous.
await
s aTask<T>
to convert it toT
async
/await
instead, which makes the async parts of the execution explicit.