Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide some terms to block in parallel steps. #36

Open
jyasskin opened this issue Feb 12, 2015 · 5 comments
Open

Provide some terms to block in parallel steps. #36

jyasskin opened this issue Feb 12, 2015 · 5 comments
Assignees

Comments

@jyasskin
Copy link
Contributor

The terms in this guide are currently all aimed at execution in steps that block the main event loop, so they transform promises instead of waiting on them. However, lots of steps run in parallel, where blocking is acceptable. In those contexts, it would be nice to be able to simply wait for a promise to settle and then respond to the result. For some examples of this, search for "settles" in http://www.w3.org/TR/service-workers/.

@domenic
Copy link
Member

domenic commented Feb 13, 2015

Is the issue here that you don't think the phrase "wait until p settles" has a precise enough definition?

What definition would you suggest? Since this isn't something that can be accomplished in JavaScript itself, I'm not sure how to define it.

@jyasskin
Copy link
Contributor Author

I don't know if it's precise enough. If it is, then just an example might suffice.

I'd probably crib the definition from https://html.spec.whatwg.org/multipage/webappapis.html#await-a-stable-state:

When an algorithm running in parallel is to wait until a promise p settles, the user agent must create a function onSettled that runs the following steps, call p.then(onSettled, onSettled), using the initial value of Promise.prototype.then, and must then stop executing (execution of the algorithm resumes when the promise settles, as described in the following steps):

  1. Resume execution of the algorithm in parallel, if appropriate, as described in the algorithm's steps.

Then there should be an example of using "If promise p is fulfilled with value v" to bind v to the value of p's [[PromiseResult]] internal slot, and an example of using "If promise p is rejected with reason r" to bind r to the value of p's [[PromiseResult]] internal slot. http://people.mozilla.org/~jorendorff/es6-draft.html#sec-promise-objects defines the "is fulfilled" and "is rejected" terms. It would probably also be useful to define "the value of p" and "the rejection reason of p" for use in "Otherwise" clauses that don't explicitly test "is fulfilled/rejected".

On the other hand, it might be better to do something other than the straightforward approach above. I'm finding that a lot of algorithms want to propagate rejection from any promise in their "in parallel" section to the promise they return, and it's easy to forget to write "If subPromise is rejected with reason r, reject mainPromise with r and abort these steps." I might like to write:

Let value be await(subPromise), propagating failures to mainPromise.

except then it won't be clear which substeps to abort on failure. Instead, how about:

  1. Let p be a new promise.
  2. Run the following steps in parallel, propagating exceptions to p.
    1. Let subPromise be the result of someFunction().
    2. Let value be the result of awaiting subPromise.
    3. Resolve p with value.
  3. Return p.

Then we'd define propagating exceptions to a promise p as surrounding its sub-steps with:

  1. Try running the nested sub-steps.
  2. And then, if an exception was thrown, reject p with the exception.

And awaiting similar to ES7:

When an algorithm running in parallel is to compute "the result of awaiting a promise p", the user agent must do the following:

  1. Let onFulfilled be a new function object whose behavior when invoked is as follows:
  2. Resume execution of the algorithm in parallel, with "the result of awaiting a promise p" computed to be the argument to onFulfilled.
    1. Let onRejected be a new function object whose behavior when invoked is as follows:
  3. Resume execution of the algorithm in parallel, with "the result of awaiting a promise p" computed to throw the argument to onRejected.
    1. Call p.then(fulfillmentHandler, rejectionHandler), using the initial value of Promise.prototype.then.
    2. Stop executing. (Execution of the algorithm resumes when the promise settles, as described in the above steps.)

It would be nicer to let people write "Await(p)" like EcmaScript does, rather than "the result of awaiting p" like most Web standards do, but that's a separable argument.

@domenic
Copy link
Member

domenic commented Feb 17, 2015

I'd probably crib the definition from https://html.spec.whatwg.org/multipage/webappapis.html#await-a-stable-state:

Ah this is helpful, yeah.

It would probably also be useful to define "the value of p" and "the rejection reason of p" for use in "Otherwise" clauses that don't explicitly test "is fulfilled/rejected".

Do you think those terms are ambiguous as-is?

I'm finding that a lot of algorithms want to propagate rejection from any promise in their "in parallel" section to the promise they return, and it's easy to forget to write "If subPromise is rejected with reason r, reject mainPromise with r and abort these steps."

This is best handled with fulfillment transformations. Then you get rejection propagation for free.

Instead, how about:

I think you were probably trying to just give an example usage, but this kind of thing is exactly what we're trying to avoid. That would be better written as

  1. Return the result of promise-calling someFunction()

As such I am hesitant of handing out foot-guns if it's going to encourage this kind of code.

In general you should almost never create a promise, wait for another promise, then resolve the original promise. If that is the envisioned use case for "awaiting" or "blocking" then we should not have any term for it.

@jyasskin
Copy link
Contributor Author

[Sorry for the essay here. I don't know the right answer, so I'm mentioning partial solutions and their problems.]

It would probably also be useful to define "the value of p" and "the rejection reason of p" for use in "Otherwise" clauses that don't explicitly test "is fulfilled/rejected".

Do you think those terms are ambiguous as-is?

I haven't thought of another plausible meaning, but in writing a spec I'm hesitant to use undefined terms.

I'm finding that a lot of algorithms want to propagate rejection from any promise in their "in parallel" section to the promise they return, and it's easy to forget to write "If subPromise is rejected with reason r, reject mainPromise with r and abort these steps."

This is best handled with fulfillment transformations. Then you get rejection propagation for free.

"Transforming p with a fulfillment handler" results in an extra level of indentation for each promise awaited in the algorithm, as https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#cache-addAll-method shows. Jungkees could also have written that as:

  1. Let resultPromise be the result of transforming p with a fulfillment handler that, when called with argument responseArray, performs the following substeps in parallel:
    1. 10 steps here
    2. Return the result of running Batch Cache Operations algorithm passing operations as the argument.
  2. Let responseBodiesPromise be the result of transforming resultPromise with a fulfillment handler that, when called with argument responses, performs the following substeps in parallel:
    1. 19 steps here
    2. Return the result of waiting for all of responseBodyPromiseArray.
  3. Return the result of transforming responseBodiesPromise with a fulfillment handler that returns undefined.

But this has the downside of widely separating the name of the intermediate variable ("Let X be the result of transforming...") from its initialization ("Return the result of...").

Instead, how about:

I think you were probably trying to just give an example usage, but this kind of thing is exactly what we're trying to avoid. That would be better written as

  1. Return the result of promise-calling someFunction()

Yes, it was just a usage example for more complex cases where the obvious optimizations fail.

As such I am hesitant of handing out foot-guns if it's going to encourage this kind of code.

We already have the "Upon fulfillment" footgun, which encourages actually-incorrect specs (see every use in Service Worker as of 2015-02-12). Await would at worst encourage stylistically bad specs.

But maybe we can make it better. The pattern of "continue running these steps" or "run the remaining steps" is widely used in HTML, so maybe:

  1. Transform promise using the remaining steps as its fulfilment handler, binding its value to newVariable.

Unfortunately, we still have an asymmetry between the first promise and subsequent ones:

  1. Let p be a new promise.
  2. Run the following steps in parallel.
    1. Do something that can block. (Fend off Domenic's optimizations)
    2. Let q be the result of someAlgorithm.
    3. Resolve p with the result of transforming q using the remaining steps as its fulfillment handler, binding its value to var.
    4. Let r be the result of nextAlgorithm(var).
    5. Return the result of transforming r using the remaining steps as its fulfillment handler, binding its value to var2.
    6. Return someOtherAlgorithm applied to var2.
  3. Return p.

The "Resolve p" vs "Return" inconsistency prevents defining "Transform promise" to return, since returning isn't always right. If we can resolve that, "Return the result of transforming X using the remaining steps as its fulfilment handler, binding its value to Y" is still quite verbose. "Await X as Y" could just be shorthand for that. (Once we're inside a fulfillment handler anyway, we don't need the explicit "propagating exceptions to P", since .then() does it.)

For reference, my first use of "wait for X to settle" is at https://webbluetoothcg.github.io/web-bluetooth/#dfn-create-a-bluetoothgattcharacteristic-representing, and Service Workers use it in several places, some of which could be optimized. At least one of Service Worker's uses is also incorrect, but that would be more clear with documentation in the promises-guide that we can only use "wait to settle" inside an "in parallel" block.

@slightlyoff
Copy link
Member

+1 for @jyasskin's hope for a better shorthand.

I know that @domenic wants the guide to help produce coherent specs that make sense to both spec authors and implementers. We're both a bit burned by the way WebIDL hurt understandability by removing flexibility, so working back towards providing good shorthands requires overcoming some learned aversion.

@plinss plinss added this to the 2020-06-01-week milestone May 4, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants