Would it be possible to add “await on first use” to the language?
We already have that feature in the language: it’s called await
. Just rewrite the example like so, instead of using /* pause to await x */
comments:
async function makePizza(sauceType = 'red') {
let dough = makeDough();
let sauce = await makeSauce(sauceType);
let cheese = grateCheese(sauce.determineCheese());
dough = await dough;
dough.add(sauce);
dough.add(await cheese);
return dough;
}
This way, instead of random punctuation like the "." operator causing your program to await... it's the actual await
keyword.
To clarify, the idea is to declare and kick off all the concurrent tasks upfront (using local variables and the ‘lazy await’ keyword), and then just continue writing the rest of the code ‘as if all the promises are resolved’. The async function automagically pauses whenever needed, so it’s no longer necessary to insert await operators throughout the code.
I admit, this is wishful thinking. I’m just waiting for someone to tell me that it’s not feasible or that it would lead to some very bad code patterns :)
So you'd be imagining something that would create a variable that would automatically await when accessed, like
async function makePizza(sauceType = 'red') {
await const dough = makeDough();
await const sauce = makeSauce(sauceType);
await const cheese = grateCheese(sauce.determineCheese());
dough.add(sauce);
dough.add(cheese);
return dough;
}
that would ensure the value is available before accessing them?
On 24 February 2017 at 16:19:03, Šime Vidas (sime.vidas at gmail.com) wrote:
To clarify, the idea is to declare and kick off all the concurrent tasks upfront
Well, that's what promises already do, even without using the
async
and await
keywords. You kick off all concurrent tasks
up-front - it's only tasks that depend on a previous task's result
that need wait around for that task to finish. Without async
functions, you'd probably do something like this:
function makePizza(sauceType = 'red') {
const dough = makeDough(), sauce = makeSauce(sauceType);
const cheese = sauce.then(s => grateCheese(s.determineCheese()));
return Promise.all([dough, sauce, cheese]).then(function([dough, sauce, cheese]) {
dough.add(sauce);
dough.add(cheese);
return dough;
}
}
With async
functions, you can avoid all those lambdas and the code's
a little bit cleaner - either way, you don't need new JS magic to do
things concurrently!
On Fri, Feb 24, 2017 at 5:18 AM, Šime Vidas <sime.vidas at gmail.com> wrote:
To clarify, the idea is to declare and kick off all the concurrent tasks upfront (using local variables and the ‘lazy await’ keyword), and then just continue writing the rest of the code ‘as if all the promises are resolved’. The async function automagically pauses whenever needed, so it’s no longer necessary to insert await operators throughout the code.
Not to be a naysayer, but I'm not a big fan of hidden behavior. If I see
sauce.determineCheese()
, the idea that the .
is triggering a
behind-the-scenes await
isn't attractive. (Granted, there are already
plenty of things it could be doing behind the scenes, with getters and
proxies; but at least they're not changing the temporal semantics of the
statement.) Domenic's version using current async
/await
syntax is nice
and clear (one might tweak the variable names a bit to differentiate
promises from resolved values, but...).
Separately, I think you're going to run into implementational complexity.
These automatic-await
values are neither promises nor resolved values,
they're a new beast with hidden await
behavior; call them "hidden
promises." Within an async
function, most but not all GetValue
operations on the variables/properties containing these hidden promises
would need an "if this is a hidden promise, await
it" guard: Any math
operation, any string operation, any object operation, any time an async
function passes the value into a non-async
function, etc. Just about the
only exception would be assignment (well, most assignments; more below),
which would just copy the hidden promise. This becomes particularly
problematic when you think about what it means to have one of these within
a structure, like an array or object; what if we then pass its container to
a non-async
function? Do we recursively search the container for
automatic-await
values and await
them before calling the non-async
function? In what order? Similarly, the return values of async
functions
would need vetting, but (arguably) only if being returned to non-async
functions, which makes for some new layer between caller and callee or an
uncomfortable awareness in the async
function of where its return value
is going. Handling all of that sounds like a lot of runtime cost to simply
hide await
from ourselves. And updating all of this in existing engines
seems like a lot of work.
Then there's the question of which assignment operations would need to
trigger a hidden await
. Presumably not let x = y;
where y
is a hidden
promise, that would largely defeat the purpose. But consider:
let x;
function example() {
a().then(() => { /* Do something */ });
}
async function a() {
async const hiddenPromise = getPromise(); // Or whatever the syntax would be...
x = hiddenPromise;
return await hiddenPromise;
}
example();
console.log(x); // What does this see?
Finally, there's the educational cost of explaining what triggers a hidden
await
to new JavaScripters.
I can imagine a new inherently-async language with this sort of thing at
its core (probably without non-async
functions at all, avoiding a lot of
the complexity above; instead making async-until-the-last-second the
default with an explicit "resolve"). It could well be quite interesting.
But for JavaScript, I think we're better off with the explicit await
.
-- T.J.
Domenic's version using current
async
/await
syntax is nice and clear (one might tweak the variable names a bit to differentiate promises from resolved values, but...).
This is the issue I have with this approach. The author is forced to create two sets of variables (for promises and resolved values), depending on the structure of the concurrent tasks. I think this is micromanagement. If the language can resolve us of this, I think that’s a convenience worth having.
Separately, I think you're going to run into implementational complexity. These automatic-
await
values are neither promises nor resolved values, they're a new beast with hiddenawait
behavior; call them "hidden promises." Within anasync
function, most but not all [GetValue][1] operations on the variables/properties containing these hidden promises would need an "if this is a hidden promise,await
it" guard: Any math operation, any string operation, any object operation, any time anasync
function passes the value into a non-async
function, etc. Just about the only exception would be assignment (well, most assignments; more below), which would just copy the hidden promise. This becomes particularly problematic when you think about what it means to have one of these within a structure, like an array or object; what if we then pass its container to a non-async
function? Do we recursively search the container for automatic-await
values andawait
them before calling the non-async
function? In what order? Similarly, the return values ofasync
functions would need vetting, but (arguably) only if being returned to non-async
functions, which makes for some new layer between caller and callee or an uncomfortable awareness in theasync
function of where its return value is going. Handling all of that sounds like a lot of runtime cost to simply hideawait
from ourselves. And updating all of this in existing engines seems like a lot of work.
I’m not sure why it would need to be that complicated. These lazy-await variables would resolve to a value on first use, in any context (first use = first appearance of the variable in the code). With that logic, the variables could be treated as resolved values (i.e. the author would view them as values, not promises) - it’s just that they’re lazy-resolved, so instead of blocking early, they block late, allowing multiple concurrent tasks to be kicked off at the beginning of the async function, without having to micromanage their promises. That’s the key, I think - the benefits of concurrent tasks without the burden of managing promises.
Finally, there's the educational cost of explaining what triggers a hidden
await
to new JavaScripters.I can imagine a new inherently-async language with this sort of thing at its core (probably without non-
async
functions at all, avoiding a lot of the complexity above; instead making async-until-the-last-second the default with an explicit "resolve"). It could well be quite interesting.But for JavaScript, I think we're better off with the explicit
await
.
In case it’s not clear, this wouldn’t change how await works or be a replacement of it. Await is great, especially for sequential async operations. So, everyone, continue using explicit await. But this new ‘lazy-await variable’ would enable new code patterns - it would allow us to get multiple async values in parallel, without having to micromanage their promises. I think, there’s a real value here.
Yes, I think you nailed it. I didn’t make the connection before. Instead of awaiting upfront, forcing the async operations to run in sequence, the awaits are ‘moved’ to the variables themselves, allowing the async ops to run in parallel (as much as possible), and once one such variable is used (in any form), the async function makes sure to pause execution in order to wait for the value to resolve.
As I’ve said elsewhere in this thread, we would get optimal concurrency without having to micromanage the promises.
On Fri, Feb 24, 2017 at 6:35 PM, Šime Vidas <sime.vidas at gmail.com> wrote:
Domenic's version using current
async
/await
syntax is nice and clear (one might tweak the variable names a bit to differentiate promises from resolved values, but...).This is the issue I have with this approach. The author is forced to create two sets of variables (for promises and resolved values), depending on the structure of the concurrent tasks. I think this is micromanagement.
Controlling the temporal mechanics of my functions seems pretty macro to me. :-) We just appear to have different perspectives on it, which is fair enough.
Separately, I think you're going to run into implementational complexity. [snip]
I’m not sure why it would need to be that complicated. [snip]
That would be simpler, yes, some kind of flag on the binding in the lexical
env object perhaps. It would also be a lot less powerful (though I suppose
if you need power, keep the promise). It seems like it would encourage
people to prefer large functions to small ones working together, though, so
they can avoid passing the hidden promise into/out of a call, triggering
the hidden await
. Which, again, you could address by keeping the promise
and passing it around instead, but then that negates the utility you're
suggesting (not managing the process directly). And programmers don't need
more inducement to bad habits. (I'm particularly guilty of this particular
bad habit. But I'm trying...)
I'm also troubled by how this moves the exception point if the promise
rejects, from the nice clear await
expression to when the side-effect of
use occurs. I suppose that's part of my overall concern about hidden
behaviors, though.
In case it’s not clear, this wouldn’t change how await works or be a replacement of it.
Just for the avoidance of doubt: You mean it wouldn't be await
, but
something await
-like but distinct, right? autoawait
, await*
,
lateawait
, jitawait
, something.
Again, I find it a really interesting idea, perhaps as a foundational concept for a massively-async language. I'm just not sure about it for JavaScript.
Anyway, those are my thoughts, FWIW. I'll lurk for a bit. :-)
-- T.J.
Note that you can also await the same promise multiple times, so you could do it like this:
async function makePizza(sauceType = 'red') {
let dough = makeDough(); let sauce = makeSauce(sauceType); let cheese = grateCheese((await sauce).determineCheese());
(await dough).add(await sauce); (await dough).add(await cheese);
return (await dough); }
Now the variables will be awaited on first use
Marius Gundersen
On 24 Feb 2017 03:36, "Šime Vidas" <sime.vidas at gmail.com> wrote:
Daniel Brain from PayPal has written a post about async/await: medium.com/@bluepnume/even-with-async-await-you- probably-still-need-promises-9b259854c161
It revolves around writing an async function which would execute three tasks in parallel like so:
|--------- dough ---------> |---- sauce ----> |-- cheese -->
The code used as a starting point was:
async function makePizza(sauceType = 'red') {
let dough = await makeDough(); let sauce = await makeSauce(sauceType); let cheese = await grateCheese(sauce.determineCheese());
dough.add(sauce); dough.add(cheese);
return dough; }
This pattern, of course, cases the tasks to execute in sequence, like so:
|-------- dough --------> |-------- sauce --------> |-- cheese -->
The remainder of the post introduces several solutions in an attempt to achieve optimal concurrency. For instance, the author’s preferred solution uses a custom memoize function and Promise.all.
Compared to the initial code above, these solutions seem complex, almost as if the language does not have the appropriate syntactic forms and/or APIs to address this particular use case.
This got me thinking. What if there was a version of await that doesn’t pause execution on the spot, but continues execution until the variable which the await is assigned to, is first referenced?
I’ve added comments to mark the positions where execution pauses:
async function makePizza(sauceType = 'red') {
let dough = await makeDough(); let sauce = await makeSauce(sauceType); let cheese = await grateCheese(/* pause to await sauce */ sauce.determineCheese());
/* pause to await dough / dough.add(sauce); dough.add(/ pause to await cheese */ cheese);
return dough; }
Please let me know if adding a await-like keyword that works like this would be a bad idea. From a layman’s perspective, it seems that having this feature would simplify code patterns which involve execution in parallel like the example described in the article.
Daniel Brain from PayPal has written a post about async/await: medium.com/@bluepnume/even-with-async-await-you-probably-still-need-promises-9b259854c161
It revolves around writing an async function which would execute three tasks in parallel like so:
The code used as a starting point was:
async function makePizza(sauceType = 'red') { let dough = await makeDough(); let sauce = await makeSauce(sauceType); let cheese = await grateCheese(sauce.determineCheese()); dough.add(sauce); dough.add(cheese); return dough; }
This pattern, of course, cases the tasks to execute in sequence, like so:
The remainder of the post introduces several solutions in an attempt to achieve optimal concurrency. For instance, the author’s preferred solution uses a custom memoize function and Promise.all.
Compared to the initial code above, these solutions seem complex, almost as if the language does not have the appropriate syntactic forms and/or APIs to address this particular use case.
This got me thinking. What if there was a version of await that doesn’t pause execution on the spot, but continues execution until the variable which the await is assigned to, is first referenced?
I’ve added comments to mark the positions where execution pauses:
async function makePizza(sauceType = 'red') { let dough = await makeDough(); let sauce = await makeSauce(sauceType); let cheese = await grateCheese(/* pause to await sauce */sauce.determineCheese()); /* pause to await dough */ dough.add(sauce); dough.add(/* pause to await cheese */ cheese); return dough; }
Please let me know if adding a await-like keyword that works like this would be a bad idea. From a layman’s perspective, it seems that having this feature would simplify code patterns which involve execution in parallel like the example described in the article.