Would it be possible to add “await on first use” to the language?

classic Classic list List threaded Threaded
10 messages Options
Reply | Threaded
Open this post in threaded view
|

Would it be possible to add “await on first use” to the language?

Šime Vidas
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.

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

RE: Would it be possible to add “await on first use” to the language?

Domenic Denicola
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.

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Would it be possible to add “await on first use” to the language?

Šime Vidas
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 :)

On Fri, Feb 24, 2017 at 3:51 AM, Domenic Denicola <[hidden email]> wrote:
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.



_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Would it be possible to add “await on first use” to the language?

Logan Smyth
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 Thu, Feb 23, 2017 at 9:18 PM, Šime Vidas <[hidden email]> 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.

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 :)

On Fri, Feb 24, 2017 at 3:51 AM, Domenic Denicola <[hidden email]> wrote:
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.



_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss



_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Would it be possible to add “await on first use” to the language?

Danielle McLean
In reply to this post by Šime Vidas
On 24 February 2017 at 16:19:03, Šime Vidas ([hidden email]) 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!
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Would it be possible to add “await on first use” to the language?

T.J. Crowder-2
In reply to this post by Šime Vidas
On Fri, Feb 24, 2017 at 5:18 AM, Šime Vidas <[hidden email]> 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][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 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:

```js
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.


_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Would it be possible to add “await on first use” to the language?

Šime Vidas
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 hidden `await` behavior; call them "hidden promises." Within an `async` 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 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.

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.



_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Would it be possible to add “await on first use” to the language?

Šime Vidas
In reply to this post by Logan Smyth
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.





_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Would it be possible to add “await on first use” to the language?

T.J. Crowder-2
In reply to this post by Šime Vidas
On Fri, Feb 24, 2017 at 6:35 PM, Šime Vidas <[hidden email]> 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.

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Would it be possible to add “await on first use” to the language?

Marius Gundersen
In reply to this post by Šime Vidas
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" <[hidden email]> wrote:
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.

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss



_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss