Or: two-color functions.
The problem: async
is infectious.
There's a famous essay about this, called What Color is your Function, which I recommend reading.
For example, say you're writing a bundler. It needs to fetch resources, but is agnostic to how those resources are fetched, so it takes a readResource
function from its caller. In some contexts the caller might have those resources synchronously available; in others not. That is, caller might have a synchronous readResource
which returns a resource immediately, or an async readResources
which returns a promise for a resource, which will need to be unwrapped with await
.
You can't easily write single function which handles both cases. You either need to write two functions or, more likely, just say your function is always async
, even when called in a way which would require no async
behavior.
And choosing to make your function async
make it unsuitable for use in any synchronous code. So that code is going to need to become async
too in order to use your function, which is at best annoying and at worst impossible.
This is bad.
A new syntax for defining functions which are optionally asynchronous, depending on how they're called. Instead of await
, these contain await?
, which behaves exactly like await
when the function is called async-style, and is a no-op when called sync-style.
In that way, you can write a single function literal which is usable in both sync and async contexts (although it would define two functions).
E.g.:
async? function bundle(entrypoint, readResource) {
let dependencies = parseForDependencies(entrypoint);
for (let dependency of dependencies) {
let result = await? readResource(dependency);
}
// etc
}
// in synchronous contexts:
let result = bundle.sync(entrypoint, fs.readFileSync); // readFileSync synchronously returns a resource
console.log(result); // not a Promise
// in async contexts:
let result = bundle.async(entrypoint, fetch); // fetch returns a promise for a resoruce
console.log(result); // a Promise
-
There should be a way for the function to switch on whether it is called as async, e.g. a
function.async
meta-property:let read = function.async ? fs.promises.readFile : fs.readFileSync
. -
for await? (x of y)
would work similarly: exactly likefor-of
when called assync
, exactly likefor-await
when called asasync
. -
Assume the obvious extension to
async? function*
generators. -
Result of the function definition is a
{ sync, async }
pair, not itself a callable. Both properties are regular functions. -
There is no reflection involved - whether the function is
async
depends only on how it's called, not what values are passed to it.