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

Current suggested async syntax feels *very* confusing #83

Closed
tabatkins opened this issue Jan 18, 2018 · 47 comments
Closed

Current suggested async syntax feels *very* confusing #83

tabatkins opened this issue Jan 18, 2018 · 47 comments

Comments

@tabatkins
Copy link
Collaborator

Per the slides for the upcoming tc39 meeting, the proposed syntax for handling async functions in a pipeline is:

"foo"
|> bar
|> await baz
|> qux

Which, assuming baz is an async function, will await it's return value before chaining to qux.

The slides explicitly call out that (await baz) would do something completely different - it would await the baz value (assumed to be a promise) then pipeline to the function it resolves to.

This seems incredibly confusing! I don't have a great suggestion to sugar this case instead, but this particular approach seems v bad. Adding parentheses shouldn't change behavior beyond just forcing operator precedence.

@mAAdhaTTah
Copy link
Collaborator

See #66 for extensive conversation, also related to #53 #56.

@tabatkins
Copy link
Collaborator Author

Right, #66 ended with the conclusion that "foo" |> bar |> await |> baz was better, and that's the version that was merged, which is why the slides confuse me in asserting that "foo" |> bar |> await baz is what we should go with.

(The overall problem is that plain "calling" of a function on a value isn't always sufficient, as the value is sometimes inside of a data structure and you instead want to instead map into it and extract it from that, to return it to being a plain value. This extends further than just Promises; those just happen to be the only copointed functor we've blessed with language syntax. ^_^ In other words, there's a more generic solution waiting to be produced here.)

@gilbert
Copy link
Collaborator

gilbert commented Jan 18, 2018

@tabatkins For study, can you provide examples where this syntax causes problems?

@zenparsing
Copy link
Member

I agree. Differentiating semantics based on the presence of parentheses is going to be a clear compositionality hazard.

@tabatkins
Copy link
Collaborator Author

In an expression like

"foo"
|> bar
|> await baz
|> qux

If I don't know about this new feature but do know how await works, this looks for all the world like I'm awaiting on baz (a Promise), and will then pipeline to the function that it fulfills with. This is a reasonable conclusion, because await baz is an expression with that exact meaning, and other expressions work as expected; "foo" |> funcMap.get("someFunc") definitely calls the expression funcMap.get("someFunc") and then pipelines to the result.

What this syntax is really trying to do is produce a new operator, the |> await operator, which has its own semantics and behavior similar to, but distinct from, the |> operator. And that's fine, in abstract! But this particular syntax clashes terribly with the syntax of pipeline to an await expression, and that's bad.

(I'm fine with "foo" |> bar |> await |> baz; it's a little clumsy but not confusing. I'm also fine with a more explicit "bigger operator" like "foo" |> bar |await> baz which doesn't clash with other readings. Finally, as I said in my previous comment, this is actually a special case of a more general problem anyway, and it would be nice to explore that more general problem rather than special-casing this one case of it. This doesn't have to slow down the pipeline proposal in general; moving forward without adding any special behavior for async functions is just fine with me.)

@TehShrike
Copy link
Collaborator

I agree. You can find my dissent in the issues @mAAdhaTTah linked to above.

I think that tacking async onto this solid proposal is a bad route in general.

You make a good point that it's an attempt to introduce a new operator. I would rather that this new async/pipeline operator be added in a separate proposal, after this traditional pipeline operator is successfully added.

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Jan 18, 2018

moving forward without adding any special behavior for async functions is just fine with me.

My understanding is the committee is not amenable to this approach.

I'm fine with "foo" |> bar |> await |> baz

This was my original suggestion, but we ran into spec problems with "dangling await":

"foo" |> bar |> baz |> await

was difficult to spec, although I can't remember the details there. @littledan raised those concerns.

@gilbert
Copy link
Collaborator

gilbert commented Jan 18, 2018

To further summarize:

  1. TC39 members seems ok with the difference between |> await f and |> (await f).
  2. The |> await |> ... syntax might be an option if we can come up with a grammar solution

@tabatkins
Copy link
Collaborator Author

It looks like it'll get relitigated at this upcoming meeting - there was a lot more grumbling about it at our pre-meeting today.

If dangling-await is truly problematic for the |> await |> syntax, and we can't wait on figuring this out later, I'm 100% fine with |await> or similar. It makes the fact that this is a new operator (related to, but not identical to, the other pipeline operator) very explicit. I don't want to block the functionality, I just really really hate how it's currently spelled.

littledan added a commit to tc39/agendas that referenced this issue Jan 19, 2018
- Pipeline operator: There are some significant, recently filed issues with the proposal that need to be resolved before Stage 2 including tc39/proposal-pipeline-operator#82 and tc39/proposal-pipeline-operator#83 (as well as the arrow function grammar). I plan to follow up on these offline.
- Extensible literals: Changes to the proposal to incorporate feedback from the last presentation are still needed, but it's too close to the meeting to let others review these changes.
@zenparsing
Copy link
Member

It seems to me that we might be able to avoid the syntax difficulties by implementing the desired await chaining with the bind operator.

Let's say that we introduce a special function-like production that can appear only after :::

BindExpression:
    LeftHandSideExpression :: await ( )

The semantics would be to simply await the value of the left operand.

The required parenthesis would allow us to avoid the dangling-await problem with |> await.

This would work with regular method chaining as well:

new DBConnection(connectionString)
  .open()
  ::await()
  .query(sql)
  ::await();
  ::displayQueryResults();

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Jan 19, 2018

So would this:

"foo" |> bar |> baz |> await

become:

"foo" |> bar |> baz::await()

or maybe even without the function-like production:

"foo" |> bar |> baz::await

?

@zenparsing
Copy link
Member

Ah, it looks like there would be a precedence issue when trying to combine the two grammars together. As it stands, you'd have to parenthesize, which isn't what you want:

("foo" |> bar |> baz)::await()

@littledan
Copy link
Member

FYI I took optional chaining off of the January agenda so we can work more on this offline before proposing for Stage 2.

@gilbert
Copy link
Collaborator

gilbert commented Jan 19, 2018

The |await> syntax is starting to grow on me.

Would the following make the |> await option easier to spec?

  1. Treat |> await as an operator
  2. Disallow normal expressions after this operator, e.g. |> await promise throws a syntax error
  3. Allow parenthesis to await normally, e.g. allow |> (await promise)
  4. Treat |> await |> as another operator
  5. Allow the usual after this operator, e.g. allow promise |> await |> f

Use case examples:

// Good (#4)
fetch(opts) |> await |> handle

// Good (#3)
x |> (await getAsyncFunc)

// Syntax Error, due to point #2
x |> await getAsyncFunc

// Syntax Error obviously, but for the same reason as above
f() g()

// Good (#2)
fetch(opts) |> await // <-- No ASI hazard
g()

@not-an-aardvark
Copy link

For what it's worth, |await> is already valid syntax:

var await = 1, foo = 2, bar = 3;

foo |await> bar; // parses as (foo | (await > bar))

@bakkot
Copy link

bakkot commented Jan 19, 2018

@not-an-aardvark only outside of an async function, whereas presumably |await> would only be valid inside of one. Not ideal, but not necessarily a blocker.

@littledan
Copy link
Member

The particular problem with "dangling await" is that it's a new ASI hazard. If you had something like this:

let x = a |> f |> await
g(x)

This would actually be awaiting g(x) and applying that to the value of the pipeline rather than awaiting f(a). A few options:

  • Say |> await can't trail. I didn't like that option much because it seems like a trailing await would be just as useful as an infix one.
  • Switch to |await>. Same disadvantage.
  • Ban await expr in a pipeline. This means that, when the ASI hazard occurs, you get a SyntaxError. You could get around it by inserting parens.
  • Just accept the new ASI hazard. Maybe this is OK with TC39's new recommendation to use semicolons?

@jridgewell
Copy link
Member

Option 3 or 4 is fine with me. I dislike |await>, it's just damn ugly.

@btoo
Copy link
Contributor

btoo commented Jan 20, 2018

I was under the impression that |await> would be used before the async function:

a |> f            // f(a)
a |> await f      // (await f)(a)
a |await> f       // await f(a)
a |await> await f // await (await f)(a)

Wouldn't this syntax then dispel the ASI hazards while also allowing us to keep the unambiguity of evaluating await expressions? I get that it's kind of ugly lol but I feel like even something syntactically similar (or at least not banning await altogether) warrants further discussion due to the fact that asynchronicity is such an integral part of modern js applications.

A problem I'm noticing now, however, is that this is antithetical to the idea of reading left-to-right

@tabatkins
Copy link
Collaborator Author

val |await> f is no more hostile to left-to-right reading than val |> await f is. ^_^

@tabatkins
Copy link
Collaborator Author

The ultimate problem here is that await is a single-arg keyword, while the pipeline operator wants single-arg functions. The val |> f |> await syntax proposal solves this by just pretending that we can treat a lone await as a function for this purpose. I like the semantics of this.

@mAAdhaTTah
Copy link
Collaborator

I like option 3. Avoid the ASI hazard with some extra syntax.

@not-an-aardvark
Copy link

Is there a difference between val |> f |> await and await val |> f? It seems like a trailing await could always be replaced with a preceding await.

@charmander
Copy link

@not-an-aardvark Do you mean await (val |> f)? There’s no difference, no.

@zenparsing
Copy link
Member

I also think banning await expr would be a fine solution.

@mariusschulz
Copy link

mariusschulz commented Jan 20, 2018

Frankly, I’m not a huge fan of the |await> naming. Currently, all operators consist of either only symbols or only letters, but not both. |await> doesn't look ”JavaScripty”, for lack of a better word.

Would it help if we required partial application syntax when using await within a chain? This might solve the dangling await issue:

"foo"
|> bar
|> await baz(?)

"foo"
|> bar
|> await baz(?)
|> qux

I would try not to ban await entirely, given the asynchronous nature of modern JavaScript. I’d rather tolerate a new ASI hazard. (Developers who prefer not to use semicolons already have to memorize all the weird edge cases; they might as well memorize one more if it means that we don't have to forbid await.)

@littledan
Copy link
Member

@mariusschulz What did you think of the x |> f |> await possibility?

@mariusschulz
Copy link

@littledan I think it looks clean and reads very nicely. I’d be happy with this syntax as well, despite the potential ASI hazard.

@pitaj
Copy link

pitaj commented Jan 20, 2018

I disagree with the idea that this proposal should not be concerned with async functions. I think that await should definitely be included in this proposal.

My favorite syntax this far is:

a
|> fetch
|> await
|> json
|> await
|> action

I think it makes much more sense than |> await foo and looks nicer than foo |await>

@gilbert
Copy link
Collaborator

gilbert commented Jan 21, 2018

Hey all, I meant for my bullet points to be treated as a single, collective solution. In other words, the solution involves enforcing all 5 points. It's intended to provide |> await syntax without an ASI hazard, as well as being easier to spec.

@pitaj
Copy link

pitaj commented Jan 21, 2018

Can you clarify what you're referring to? I'm having a hard time finding the bullet points you're referring to.

@littledan
Copy link
Member

littledan commented Jan 21, 2018

@gilbert Thanks for the clarification; now that I re-read it, I think we're converging on your proposal. I'll try to write it up in spec-ese within a few weeks.

@pitaj I think @gilbert is referring to this comment: #83 (comment)

@pitaj
Copy link

pitaj commented Jan 21, 2018

@littledan Ah, thank you. In that case, then yes I agree with @gilbert 's solution

@Alexsey
Copy link

Alexsey commented Jan 25, 2018

Guys, there are more generic cases than just await (await f)(a). Consider await (await obj.f()).g(a) I think examples of proposed syntax should include resolution at list to such case or similar. Here are another ways to handle initial problem

  1. await |>
    (await f)(a) -> a await |> f
    (await obj.f())(a) -> a (await |> obj.f())
    (await obj.f()).g(a) -> a ((await |> obj.f()) g)
    await (await obj.f()).g(a) -> a ((await |> obj.f()) await g)
  2. awaited
    (await f)(a) -> a |> awaited f
    (await obj.f())(a) -> a |> awaited obj.f()
    (await obj.f()).g(a) -> a |> (awaited obj.f()).g
    await (await obj.f()).g(a) -> a |> await (awaited obj.f()).g

Actually await |> is the same as |await> so this examples should be relevant for |await> as well

@pitaj
Copy link

pitaj commented Jan 25, 2018

@Alexsey Your examples would be written like this with the proposal by @gilbert

// these don't even need any pipeline await syntax
(await f)(a) === a |> (await f)
(await obj.f())(a) === a |> (await obj.f())
(await obj.f()).g(a) === a |> (await obj.f()).g

// this one is the only one where it's useful
await (await obj.f()).g(a) === a |> (await obj.f()).g |> await

This is actually simpler than what you have written. In fact, what you have written makes no sense. You examples all involve the results of awaiting either being functions, or having members that are functions. This issue is about functions which return promises being in the pipeline.

It's not about (await f)(a). It's about await f(a). You seem to be proposing some kind of weird operator that has zero use. In (2), you have awaited, but you're using it exactly as you would use await. You can literally just change awaited to await and almost every example you provided is compatible with a non-await pipeline operator.

@Alexsey
Copy link

Alexsey commented Jan 25, 2018

@pitaj as I sad before this proposal is equivalent to |await> but now I can see that @gilbert is really look better

@jaufgang
Copy link

jaufgang commented Aug 4, 2018

What about simply introducing a new "asynchronous pipeline" or "await pipeline" operator that is semantically equivalent to the proposed |await> but less ugly & less verbose?

I would propose adopting an operator that looks similar to the synchronous pipeline operator, but that could be interpreted to visually suggests asynchronicity or delay? Something such as ||> or ]> or }>

So instead of

"foo" |> bar |> await |> baz;

or

"foo" |> bar |await> baz;

We could have one of these (Or something similar. Any other ideas?)

"foo" |> bar ||> baz;
"foo" |> bar ]> baz;
"foo" |> bar }> baz;
"foo" |> bar :> baz;
"foo" |> bar |:> baz;  

@thysultan
Copy link

thysultan commented Aug 4, 2018

Since this would also affect typeof and any similar future operators, how would those get treated?

@rpamely
Copy link

rpamely commented Aug 5, 2018

I've had thoughts around this too. How do people feel about this idea. I think the problem with a |> await |> ... is conceptual. Taking away syntax for a minute, let's imagine pipeline was implemented as a function. A sync version might look like this:

pipe(person.score
     double,
     _ => add(7, _),
     _ => boundScore(0, 100, _));

Conceptually I think the most intuitive async version of this would be to model it after an async function and have pipe return a Promise. I'm guessing this is how most of us would implement this. Each step would automatically be awaited if you returned a Promise. For example:

// We await the pipeline as a whole since it returns a Promise
await pipeAsync(person.score
                double,
                _ => add(7, _),
                _ => isGlobalHighScoreP(_), // Returns Promise, will be awaited before next step
                _ => _ ? "You are top" : "Try again");

Do people agree that this is intuitive behaviour? If others think this is intuitive I would suggest we find a syntax that indicates the entire pipeline is async. One immediate idea is to use the async keyword but I don't want to dwell on syntax yet until we determine what the intuitive behaviour is.

I think this is why a few people are stumbling at the current syntax because it is like you are doing the following, which I don't think is intuitive:

// (NOT what I am proposing)
pipe(person.score
     _ => isGlobalHighScoreP(_),
     await, // awaiting being passed to function is not intuitive
     _ => _ ? "You are top" : "Try again");

@charmander
Copy link

I think this is why a few people are stumbling at the current syntax

Who? In a comment on this proposal, or somewhere else?

@ljharb
Copy link
Member

ljharb commented Aug 6, 2018

Would an implicitly awaited promise be a promise for the value, or a promise for the function, both?

@pitaj
Copy link

pitaj commented Aug 6, 2018

To me, await being used like a function which unwraps a Promise into the value it resolves to, in the context of an async function, is totally intuitive.

Frankly, I don't understand why any of you have a problem with the |> await |> syntax. It's a perfect analogue to the corresponding async function without pipelines.

This syntax would only be possible within async functions, it does not apply generally to dealing with promises. If you havea bunch of functions built to deal with promises, then you can use them in a normal pipeline.

@rpamely
Copy link

rpamely commented Aug 6, 2018

Would an implicitly awaited promise be a promise for the value, or a promise for the function, both?

I am imagining It would execute the function and await the value of the returned Promise. i.e. something like

async function pipeAsync (val, ...fns) {
    val = await val;
    for (const fn of fns) {
        val = await fn(val);
    }
    return val;
}

Who? In a comment on this proposal, or somewhere else?

It was the overall impression I got following the threads in the issues. Maybe I am mistaken? Do others feel that a |> await |> ... has been widely accepted?

@mAAdhaTTah
Copy link
Collaborator

Some background conversation in #66.

|await> was suggested there, although I find that syntax ugly compared to |> await, which doesn't add any new operators or forms of the current operator. This is the same issue with the alternative forms proposed above (new operators increase the complexity far more than |> await does).

Otherwise, @pitaj's comment is a good summary of why I like |> await.


I'll also note that when this issue was raised, the syntax was:

x |> await foo |> bar

which is confusing. I might actually suggest we close this issue, as the original question no longer applies to any of the current proposals.

@rbuckton
Copy link
Collaborator

Resurrecting this thread in light of a conversation with @tabatkins and @jridgewell during the last TC39 meeting. In general we were discussing both await and yield with F#-style pipelines. In summary, I was discussing the following cases

  1. We need a way to await the piped value in an async function.
  2. For consistency, we also should have a way to yield or yield* the piped value in a generator.
  3. To avoid confusion, the syntax and semantics for how await, yield, and yield* work in these contexts should be consistent.

Thus, I propose the following:

  • |> await is a special syntactical form to await the piped value.
    • If your goal is to pipe the value into a function returned from a Promise, you use parens: x |> (await pf)
    • The syntax |> await x would be illegal, and we would need to ensure this works correctly with ASI (so |> await\nf() would not be illegal, since ASI would interpret this as |> await;\nf()).
    • There are no other restrictions to using await anywhere else in the right side of the |> (e.g., x |> F(await y) is fine).
  • |> yield is a special syntactical form to yield the piped value.
    • If your goal is to pipe the value into a function sent into the generator, you use parens: |> (yield) or |> (yield y)
    • The syntax |> yield y would be illegal, and we would need to ensure this works correctly with ASI (so |> yield\nf() would not be illegal, since ASI would interpret this as |> yield;\nf()).
    • There are no other restrictions to using yield anywhere else in the right side of the |> (e.g., x |> F(yield y) and x |> F(yield) are fine).
  • |> yield* is a special syntactical form to delegate the generator protocol to the piped value.
    • If your goal is to pipe the value into a function returned by a delegated generator, you use parens: |> (yield* g).
    • The syntax |> yield* g would be illegal, and we would need to ensure this works correctly with ASI (so |> yield*\ng() would not be illegal, since ASI would interpret this as |> yield*;\nf()).
    • There are no other restrictions to using yield* anywhere else in the right side of the |> (e.g., x |> F(yield* y) is fine).

The other reason this symmetry is useful with respect to yield is that it removes the ambiguity resulting from precedence differences between |> and yield. Consider the following case without the parenthesis restriction for yield. As a developer, you might write the following:

x |> yield a |> F

Your intention might have been for the following to happen:

(x |> yield a) |> F

However, due to the precedence of the yield operator, what is actually parsed is this:

x |> yield (a |> F)

By forcing the parenthesis for inline yield, we avoid this ambiguity.

In summary (for F#-style pipes):

  • await:
    • x |> awaitawait x
    • x |> await |> FF(await x)
    • x |> await pfIllegal
    • x |> (await pf)(await pf)(x)
    • x |> (await pf) |> FF((await pf)(x))
  • yield:
    • x |> yieldyield x
    • x |> yield |> FF(yield x)
    • x |> yield yIllegal
    • x |> (yield)(yield)(x)
    • x |> (yield) |> FF((yield)(x))
    • x |> (yield y)(yield y)(x)
    • x |> (yield y) |> FF((yield y)(x))
  • yield*:
    • x |> yield*yield* x
    • x |> yield* |> FF(yield* x)
    • x |> yield* yIllegal
    • x |> (yield* y)(yield* y)(x)
    • x |> (yield* y) |> FF((yield* y)(x))

@tabatkins
Copy link
Collaborator Author

In F# syntax, yes, all this is good and exactly what I would want and expect.

(In Hack syntax, await and yield already work fine, tho the precedence of yield is a little problematic and means you'll usually want parens.)

@tabatkins
Copy link
Collaborator Author

Closing since this was my own issue, and it's all solved now. ^_^

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests