Skip to content
This repository has been archived by the owner on Jan 26, 2022. It is now read-only.

Disallowing arbitrary expressions as a pipeline function #2

Closed
littledan opened this issue Mar 15, 2018 · 16 comments
Closed

Disallowing arbitrary expressions as a pipeline function #2

littledan opened this issue Mar 15, 2018 · 16 comments
Assignees
Labels
discussion Discussion about a design decision

Comments

@littledan
Copy link
Member

Interesting idea here, with the "don't shoot yourself in the foot" reasoning. I agree that I'd rather not encourage currying as the main way to use the pipeline operator, but it seems like this syntax disallows that entirely. Is this intentional or desired?

@littledan
Copy link
Member Author

(To explain myself, I was sort of picturing that any expression which didn't contain # would be treated as a function.)

@js-choi
Copy link
Collaborator

js-choi commented Mar 15, 2018

Arbitrary expressions are allowed as pipeline functions—in explicit topic style. In general, x |> f(a, b) is visually ambiguous between four reasonable interpretations:

x |> f(a, b, #),
x |> f(a, #, b),
x |> f(#, a, b), and
x |> f(a, b)(#).

I want to minimize lookahead, avoid garden-path syntax, and optimize for syntactic locality. This went consideration especially into distinguishing bare style versus topic style.

The problem with having determination of pipeline style depend on the presence/absence of # is this garden-path. @tabatkins and I discussed this on es-discuss a few days ago. It would probably not be good for readers to have to search value |> compose(f, g, h, i, j, k, #, l, m, n, o) or value |> compose(x['hello'][symbol].propertyA.key(#).propertyB.propertyC, anotherFunction)—for the absence or presence of a single topic reference #—simply to determine whether the pipeline will evaluate into a function call or a function.

In addition, changing the grammar to depend on containment of a token would make it impossible to parse with a context-free grammar or by introducing a new grammar-production parameter, which multiplies the number of productions in JavaScript’s grammar. The pipeline syntax grammar alone would become considerably considerably more complicated – and this also applies to the grammars for every single other type of expression, all of which would have to be modified with an additional syntactic parameter.

At the very beginning, in fact, I actually considered making the grammar to depend on containment of a token – but then I realized many of the disadvantages above that it would bring…

Bare style is a special syntax, with special evaluation semantics, optimized for one common use case: unary functions. The new and await tokens are just flags for bare style because unary constructor calls and awaited unary async-function calls are also quite common, as my review of real-world codebases in Motivation shows. But in general, I expect topic style to be used more often, and topic style itself is very terse.


Bare style does not address auto-curried functions, no. But topic style covers them. This is intentional, because, in general, x |> f(a, b) is visually ambiguous between the four reasonable interpretations.

x |> f(a, b, #),
x |> f(a, #, b),
x |> f(#, a, b), and
x |> f(a, b)(#).

I probably should add this example to the proposal before TC39 next week, too. Hopefully, this reasoning makes sense.

@js-choi js-choi self-assigned this Mar 15, 2018
@tabatkins
Copy link

In addition to @js-choi's reasoning, note that the "lack of a topic reference implies that it's an expression that will evaluate to a function" means that x |> foo.bar does not work as probably intended - rather than being equivalent to foo.bar(x), it's equivalent to do{ let _ = foo.bar; _(x) } - the method loses its this connection!

It's also in direct conflict with the other nice grammar forms of bare-style; x |> new Foo would become equivalent to (new Foo)(x) rather than new Foo(x); x |> await foo would become equivalent to (await foo)(x) rather than await foo(x); etc.

In other words, you'd lose all three of these cases being easy (you're forced to use topic-style instead), in return for certain FPish cases being easier. (As an inveterate Lisper, even tho it pains me, I must admit that point-free stuff is less relevant and useful than methods/constructors/async/future stuff.) Neither approach makes any cases impossible, you just have to remember to put a (#) at the end of the ones that aren't catered to.

And I find the "no foot-shooting" aspect extremely relevant for x |> foo.bar in particular; I expect that people would write this quite a lot, and then cuss every time it failed (at run-time! in potentially confusing ways!).

@js-choi
Copy link
Collaborator

js-choi commented Mar 15, 2018

@tabatkins Thanks for the comment! I’m interested in your perspective about better communicating these aspects of the proposal in my explainer, if you have any ideas. Maybe expanding the syntactic locality goal section, or adding some paragraphs about method calls to the motivations, core proposal section…

@js-choi
Copy link
Collaborator

js-choi commented Mar 15, 2018

@tabatkins Also, per #3 I might separate the bare constructor call and the bare awaited function call from the core proposal to new additional features. If you have an opinion about that, feel free to chime in at #3.

@tabatkins
Copy link

I think those do make sense to be a (single) separate feature, yeah; it makes it clearer that this is a syntax extension point that can take on new stuff in the future too. The x |> foo.bar part is the biggest argument for bare-form and against arbitrary-expression-form anyway. ^_^

The third paragraph of syntax locality could use some expansion into a listing of the cases you talk about here: the existing talk about garden-pathing to figure out which form it's in; the x |> foo.bar argument; the x |> foo(#, y) vs x |> foo(y, #) vs x |> foo(y)(#) argument.

For that last one, pointing to other langs with pipelines, and pointing out that they differ in how they interpret this case (Elm, for example, interprets x |> foo(y) as x |> foo(y, #)), so (a) there's good arguments for any of the placements, and (b) authors might be expecting any of them, depending on where they come from.

@littledan
Copy link
Member Author

@tabatkins I don't think that's true about the current pipeline specification text--I think it would use the right receiver.

@tabatkins
Copy link

I'm curious how it would do so! Adding special parsing rules, distinct from normal JS, in what is otherwise a perfectly ordinary arbitrary expression, doesn't seem like the best idea.

In particular, I'd be very unhappy if x |> foo.bar magically worked, but x |> (foo.bar) or x |> decorate(foo.bar) continued to fail in the standard (confusing, runtime) JS way.

This proposal makes it so that the latter two cases are early syntax errors, and when you fix them by adding a topic reference, they work in the expected way, with this bound properly.

(In other words, I really think the anti-footgun measures of this proposal are valuable here, even if arbitrary-expression-form somehow handled all the use-cases otherwise.)

@js-choi js-choi removed the enhancement New feature or request label Mar 15, 2018
@mAAdhaTTah
Copy link

mAAdhaTTah commented Mar 17, 2018

I'm curious how it would do so! Adding special parsing rules, distinct from normal JS, in what is otherwise a perfectly ordinary arbitrary expression, doesn't seem like the best idea.

Because fundamentally, we're not passing the method into something else to use; it's getting applied immediately. There's isn'y really a fundamental difference between x |> g |> a.f and a.f(g(x)), and we wouldn't expect the result to be any different.

Also, x |> (foo.bar) would desugar to (foo.bar)(x), which still maintains it's receiver. Babel transpiles to (0, foo.bar)(x) in order to break the connection to the receiver.

The behavior is consistent across all the proposals at this time, and is currently specced in the "minimal proposal" in the repository at this time.

@js-choi
Copy link
Collaborator

js-choi commented Mar 17, 2018

@tabatkins:

@mAAdhaTTah is correct in that x |> (foo.bar) works correctly in even the original/minimal proposal, as well as Proposal 1: F-Sharp Style Only with await. See the (although it’s out of date; it’s going to have await removed, as I mentioned in #3; see tc39/proposal-pipeline-operator#108.

However, x |> (foo.bar) is an early Syntax Error in smart pipelines; it must either be a bare-style x |> foo.bar or a topic-style x |> (foo.bar)(#) (or x |> foo.bar(#)).

In addition, the semantics of await in both the original/minimal proposal and Proposal 1: F-Sharp Style Only are parenthesis sensitive. As mentioned in tc39/proposal-pipeline-operator#108 (comment), x |> await is different than x |> (await).

Proposal x |> await x |> (await)
Original/minimal proposal (F# only) Early Syntax Error (await)(x)?
Proposal 1 (F# only + await) await x (await)(x)
Proposal 4 (smart) Early Syntax Error Early Syntax Error

I probably should add this table to tc39/proposal-pipeline-operator#108 and to the wiki.

@mAAdhaTTah
Copy link

@js-choi x |> (await) would be a SyntaxError in both minimal & Proposal 1, because (await) is not a valid AwaitExpression, which must await something.

@tabatkins
Copy link

Okay, so the minimal proposal desugars val |> XXX (where XXX is an arbitrary expression) directly into (XXX)(val), yeah?

(I didn't realize that (foo.bar)(x) retained its receiver. Interesting! Now I'm curious why that occurs, tho. ^_^)

Okay, then that does seem to solve the fragility issues at least somewhat, nice.

@js-choi
Copy link
Collaborator

js-choi commented Mar 18, 2018

@tabatkins: I hadn’t realized this until recently myself: (foo.bar)(x) retains its receiver for the same reason that delete (foo.bar) works. The grouping operator’s evaluation algorithm does not apply the GetValue abstract operation to its subexpression’s result, even if that result is a Reference. This means that it itself may evaluate to a Reference, and this allows function calls’ evaluation algorithm to use the Reference’s base.

I’m really glad that this fragility problem doesn’t seem apply to Proposal 1—though it does put a lot of the burden of its being useful on @rbuckton’s partial-application proposal, which smart pipelines would also subsume with a similar amount of syntactic complexity. Smart pipelines also try hard to prevent visual ambiguity, requiring x |> await o.p to be distinguished as x |> await o.p(y, #), x |> await o.p(#, y), x |> await o.p(y)(#), x |> (await o.p(y))(#), x |> (await o.p)(#) or x |> await #.

Proposal Original/minimal proposal (F# only) Proposal 1 (F# only + await) Proposal 4 (smart pipelines)
x |> o.p o.p(x) o.p(x) o.p(x)
x |> (o.p) o.p(x) o.p(x) Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> o.p or
x |> (o.p)(#).
x |> o.p(y) o.p(x)(y) o.p(x)(y) Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> f(y, #),
x |> f(#, y), or
x |> f(y)(#).
x |> await Early Syntax Error: await not supported await x: await the value x Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> await #.
x |> (await) (await)(x) – Syntax Error: Unexpected ) (await)(x) – Syntax Error: Unexpected ) Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> await #.
x |> await o.p Syntax Error: await not supported (await x) o.p – Syntax Error: Unexpected o.p Without Feature BA:
Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> await o.p(#) or
x |> (await o.p)(#).
With Feature BA:
await o.p(x)
x |> (await o.p) Syntax Error: await not supported (await o.p)(x) Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> await o.p(#) or
x |> (await o.p)(#).
x |> await o.p(y) Syntax Error: await not supported (await o.p(y))(x) Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> await o.p(y, #),
x |> await o.p(#, y),
x |> await o.p(y)(#),
x |> (await o.p(y))(#),
x |> (await o.p)(#), or
x |> await #.
x |> new Syntax Error Syntax Error Syntax Error
x |> (new) (new)(x) – Syntax Error: Unexpected ) (new)(x) – Syntax Error: Unexpected ) (new)(x) – Syntax Error: Unexpected )
x |> new o.p TypeError: new o.p is not a function; it is an instance of o.p. TypeError: new o.p is not a function; it is an instance of o.p. Without Feature BC:
Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> await o.p(#) or
x |> (await o.p)(#).
With Feature BC:
new o.p(x)
x |> (new o.p) TypeError: (new o.p) is not a function; it is an instance of o.p. TypeError: (new o.p) is not a function; it is an instance of o.p. Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> await o.p(#) or
x |> (await o.p)(#).
x |> new o.p(y) TypeError: (new o.p) is not a function; it is an instance of o.p. TypeError: (new o.p) is not a function; it is an instance of o.p. Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> new o.p(y, #),
x |> new o.p(#, y),
x |> new o.p(y)(#),
x |> (new o.p(y))(#),
x |> (new o.p)(#), or
x |> new #.
x |> () => y Syntax Error: Unexpected =>. Use
x |> (() => y) instead
Undecided Early Syntax Error: Unexpected =>. Use
x |> (() => y)(#) or
x |> (() => #).
x |> (() => y) (() => y)(x) (() => y)(x) Early Syntax Error: Topic-style pipeline needs topic reference #. Use
x |> (() => y)(#) or
x |> (() => #).

@tabatkins
Copy link

Wait, your x |> await o.p entry says that's not allowed, but that's precisely what feature BA allows, right?

@js-choi
Copy link
Collaborator

js-choi commented Mar 21, 2018

@tabatkins Yeah, that table is for the core proposal only…I should note there and in the wiki that it is allowed with Additional Feature BA.

Edit 1: And I should add some entries for construction too, which exhibit more differences between Proposal 1 and 4.

Edit 2: Edits done in the wiki, #2 (comment), and #6 (comment).

@js-choi js-choi added explainer/readme About readme.md discussion Discussion about a design decision and removed spec About spec.html labels Mar 24, 2018
@js-choi js-choi removed the explainer/readme About readme.md label Mar 24, 2018
@js-choi
Copy link
Collaborator

js-choi commented Mar 8, 2021

After talking more with Tab Atkins and Daniel Ehrenberg, we’ve decided to archive this proposal in favor of a simpler Hack-pipes proposal, which is a subset of this proposal.

Feel free to open a new issue in the new proposal’s repository if you think it still applies.

@js-choi js-choi closed this as completed Mar 8, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
discussion Discussion about a design decision
Projects
None yet
Development

No branches or pull requests

4 participants