Skip to content Skip to sidebar Skip to footer

Does The Hack-style Pipe Operator |> Take Precedence Over Grouping Operator ( ) In Order Of Operations In Javascript?

Does the Hack-style pipe operator |> take precedence over grouping operator ( ) in order of operations in JavaScript? I'm investigating tc39/proposal-pipeline-operator - for Jav

Solution 1:

I don't think "the pipe operator is taking precedence over grouping parentheses" is the right way to look at this.

The OP is essentially asking how these can do the same thing:

  1. 1 |> f(%) |> g(%)
  2. 1 |> (f(%) |> g(%))

The OP sees that f(%) |> g(%) on its own cannot be a valid expression, but it seems that #2 requires evaluating this as a sub-expression, while #1 doesn't seem to require this. The OP's explanation is that in #2 the |> operator is taking precedence over the grouping parentheses, and is actually equivalent to #1 even though it does not look like it can be.

I don't like that explanation of how #2 works. Any operator "taking precedence over parentheses" sounds like confusing nonsense to me. So I want a way of conceptualising what is happening in #2 that (a) gives me the correct intuition for what the result is and (b) respects the fundamental rules of how parentheses work. The following is how I would conceptualise it after reading the proposal, and some brief experiments with the Babel implementation the OP linked.

Firstly, note that |> is not an operator in the same way that + or - or ! is an operator. All of those stand for computations in the same way that user-defined functions do; they take values as arguments. 1 + 2 is the same as x + y in a context where we have x = 1; y = 2. + operates on the value that results from evaluating its operands, and doesn't care what particular expression we wrote down in the source code for those operands.

|> isn't like this. Its arguments are expressions, not values, and the right argument isn't even a normal expression, but one that must contain a topic reference %. |> cares about code you write for its operands, not just the result of evaluating its operands. 1 |> f(%) is valid, but you can't use z = f(%); 1 |> z. %, and template expressions containing %, are not first-class values. The rules of evaluating expressions built from |> and % cannot be built by just adding new operators that take values as arguments. They need to be specifically "hard-wired" into the language, in a way that ordinary operators do not.

Notice how I've been talking about different explanations rather than whether they are correct or incorrect. To a certain extent this is a question of how you prefer to think of it, not absolute truth. |> cannot be an ordinary operator at all; it has to be part of the syntax of the language. We often think about language syntax in ways that have little to do with any actual implementation strategy, so I'm not going to say it's outright invalid to think of this case is that |> can take precedence over parentheses. If you can build a consistent set of rules around that idea, feel free to conceptualise it that way, but I will not be using that option.

However, since the language has made the choice to make |> look much like an ordinary operator, I choose to believe we're supposed to interpret expressions containing it in a fairly similar way, not totally overriding other rules of syntax like parentheses. In particular, I prefer to insist that 1 |> (f(%) |> g(%)) is 1 piped to f(%) |> g(%), not 1 piped to f(%) and then the result of that piped to g(%).

My position would fall apart if I couldn't give a consistent interpretation to what f(%) |> g(%) means. However it has an obvious meaning; in x |> 1 + % we get the result by taking the right hand side and substituting the left hand side where the % is, giving 1 + x. I say: just do the same thing with f(%) |> g(%). The result is g(f(%)), which still contains a topic reference and so is still invalid except as the right hand side of an outer |> operator.

So 1 |> (f(%) |> g(%)) ends up meaning g(f(%)), which is the same thing that (1 |> f(%)) |> g(%) means, even though it gets there by a slightly different route.

This also helps explain what is going on in the OP's example 1 |> (log(f(%) |> g(%))). Expressions containing % are not first class values and cannot be passed to log to be printed. My explanation for f(%) |> g(%) says that log(f(%) |> g(%)) means log( g(f(%) ). This is still a template expression containing %, and cannot be evaluated on its own; it must occur to the right of a |> operator. So 1 |> (log(f(%) |> g(%))) ends up meaning log( g(f(1)) ). This makes it unsurprising that the OP gets 3 printed in the console; that is not proving that f(%) |> g(%) is somehow equivalent to 3, and the fact that 1 |> 3 is a syntax error does not tell you anything about 1 |> (f(%) |> g(%)).

This is also where I suspect you'll have trouble building a consistent interpretation of the pipe syntax using the "takes precedence over parentheses" idea; you have to also start saying things like "the pipeline takes precedence over function applications, so that 1 |> log(f(%) |> g(%)) actually is interpreted as log(1 |> f(%) |> g(%)). Maybe you can carry that all the way through and have a model for pipelines that actually gives the correct results in all cases, but it seems much more complicated and confusing than is necessary than just accepting that one of the weird things about |> is that f(%) |> g(%) is an expression that itself results in a template, not a value.


Although there's nothing stopping a language designer putting confusing nonsense into the rules of their language, and JavaScript is arguably not without its share already.

Solution 2:

No, it does not take precedence over the grouping operator. However, the |> operator is right-associative, which means that

1 |> f(%) |> g(%)

and

1 |> (f(%) |> g(%))

form parse trees of the same shape:

  |>          |>
 / \         / \
1   |>      1  ( )
   / \          |
f(%) g(%)       |>
               / \
            f(%) g(%)

To get the dependency tree nested the other way round, you can use the grouping operator on the the first clause:

(1 |> f(%)) |> g(%)

becomes

    |>
   / \ 
 ( )  g(%)
  |
  |>
 / \ 
1  f(%)

Proof in Babel REPL:

transpiler output of the three expressions nested differently

Solution 3:

In response to your response :-) You posted a new line of thought that warrants a separate answer, which might however also clear up your original confusion.

It's reasonable for us to consider (f(%) |> g(%)) must have some value regardless of the evaluation order by JavaScript runtime.

The expression on the right of |> is in some ways a template for a real expression, with the % marking a "hole" that needs to be filled by something. So this is new - this is a whole new concept in the history of JavaScript.

No, this is not new at all. "A template expression with a hole that is filled by something" is basically just a function. The "hole" (called topic in the proposal) is just a constant with a local definition in the right-hand side of the |> operator. Taking basic lamda calculus syntax for introducing constants, we can say that an expression

lhs |> rhs

is syntactic sugar for

(% => rhs)(lhs)

where % is assumed to be an identifier, the parameter of the anonymous arrow function.

With this in mind, we can look at what grouping operator does to the expression 1 |> f(%) |> g(%);. Let's first realise that |> is right-associative, so the parse tree of the expression is

  |>
 / \ 
1   |>  
   / \ 
 f(%) g(%)

and is evaluated as

(% => (%x => g(%) )(f(%)) )(1);

With the grouping 1 |> (f(%) |> g(%));, nothing changes:

(% => ((%x => g(%) )(f(%))) )(1);
//    ^                   ^

but with (1 |> f(%)) |> g(%);, we get

(% => g(%) )( ((%x => f(%) )(1)) );
//            ^                ^

1 |> (f(%) |> g(%)) contains the sub-expression (f(%) |> g(%)), so clearly that must be meaningful as an expression in its own right.

The only additional restriction that the hack pipeline proposal imposes is that you can use % topic references only on the right-hand side of an |> operator. It is not valid in an arbitrary expression on its own. So while I can assign some denotational semantics to % and |> about their evaluation using lambda calculus, it is not possible for javascript programs to observe this intermediate result value, we can never refer to that arrow function I described above. This is necessary to give the compiler the freedom to optimise this properly.

In particular, when you write 1 |> (log(f(%) |> g(%))); you cannot substitute the subexpression (log(f(%) |> g(%))) with 3. The flaw in this idea is that you think by adding the log call, you would get an evaluation as

  (log(% => (%x => g(%) )(f(%)) ))(1);
// ^^^^                         ^

but actually it does become

  (% => log((%x => g(%) )(f(%))) )(1);
//      ^^^^                   ^

Solution 4:

This is my own Answer after reading Answers by @Ben and @Bergi. My comments to respond to them under their Answers will not be sufficient in terms of the complexity and readability to readers, and editing the Question will also have the same problem for the amount of the document. (the previous duplicated section has been removed from the Question and moved here)

My response to Answer by @Ben


Common ground between us: (Please feel free to express your objection)


  • Grouping parentheses mean the same thing in Haskell as they do in high school mathematics. They group a sub-expression into a single term. This is also what they mean in JavaScript.

  • think of evaluation order as a totally separate concept than grouping.

quoted from your answer: https://stackoverflow.com/a/69386130/11316608

  • 1 |> (f(%) |> g(%)) contains the sub-expression (f(%) |> g(%)), so clearly that must be meaningful as an expression in its own right.

if the grouping () shares the same functionality in mathematics, it's reasonable for us to consider (f(%) |> g(%)) must have some value regardless of the evaluation order by JavaScript runtime.

     |>
    / \ 
   1   |>  
      / \ 
    f(%) g(%)
  • The expression on the right is in some ways a template for a real expression, with the % marking a "hole" that needs to be filled by something. So this is new;

This is a whole new concept in the history of JavaScript.

  • Also note that it doesn't matter how this actually works at a lower level in any given implementation, so staring at the output of the babel transpiler is not necessarily the way to reach enlightenment.

Correct, the only reason I've used that is to prove the actual behavior, and I am also the one who is not that interested in how it works underneath.


Consequently, regardless of your analysis what's going on underneath with the Hack |> and the placeholder %, I wrote:

1 |> (log(f(%) |> g(%)));

You can check the full code in this Question.

and the result is 3, so,

1 |> (f(%) |> g(%)) ===
1 |> 3;

Apparently this does not make sense anymore, and this is a syntax-error

enter image description here

therefore, it's reasonable to observe, in order to make sense, the only way is to refuse the operation/evaluation of (f(%) |> g(%)) that I would call "the Hack-style pipe operator |> takes precedence over grouping operator ( ) in order of operations in JavaScript".

Also note that we the potential user of this Hack pipe really don't care what's going on underneath, and the Bebel transipiler implemented by the champion team just helps for the fact check.


My response to the second edition of the answer:

Common ground between us: (Please feel free to express your objection)


  • |> cannot be an ordinary operator at all; |> is not an operator in the same way that + or - or ! is an operator which stand for computations in the same way that user-defined functions do; they take expressions/values as arguments.

  • The right argument of |> isn't even a normal expression, but one that must contain a topic reference %.

  • |> looks much like an ordinary operator, so it's reasonable for us to believe to interpret expressions containing it in a fairly similar way, not totally overriding other rules of syntax like parentheses.

  • It's Not outright invalid to think of this case is that |> can take precedence over parentheses.


I somehow understand your intuition against my question, however, I also understand you no longer deny the perspective.

This is also where I suspect you'll have trouble building a consistent interpretation of the pipe syntax using the "takes precedence over parentheses" idea;

I'm afraid to say the statement is not valid.

I will not have any trouble building a consistent interpretation of the pipe syntax if I'm Not using "takes precedence over parentheses" idea.

The fact is that we will have trouble building a consistent interpretation of the pipe syntax under the estimation of "the hack pipe never takes precedence over parentheses" idea, and again, I'm afraid to say, in your answer, I feel somewhat such an aspect has been justified by repeating to show the process underneath. In fact, I already understand how it behaves underneath and share the context of your explanation. I am not confused about what you say.


EDIT: I added new information.

My position would fall apart if I couldn't give a consistent interpretation to what f(%) |> g(%) means.

My explanation for f(%) |> g(%) says that log(f(%) |> g(%)) means log( g(f(%) ).

So, before the placeholder % is sufficed by a concrete value, it is like:

f(%) |> g(%) means % => g(f(%)) that is the function composition of f and g.

In algebraic sense (the mininal/F#-style), the pipeline-operator has been explained as

f(x)    === x |> f 
g(f(x)) === x |> f |> g

and if . should be the operator for function composition,

g(f(x)) === x |> (f . g)

|> itself is not associative in Monoid.

For Hack pipe, on the other hand,

g(f(x)) === x |> f(%) |> g(%)
g(f(x)) === x |> (f(%) |> g(%))

|> seems to be associative in Monoid but cannot be in a mathematical sense. With hack |>, the binary operator for function application switch to the role of function composition once the sub-expression is closed with ().

g(f(x)) === x |> (f . g)(%)

you have to also start saying things like "the pipeline takes precedence over function applications, so that 1 |> log(f(%) |> g(%)) actually is interpreted as log(1 |> f(%) |> g(%)) ".

Correct, and when I observe (observed in really already)

1 |> log(f(%) |> g(%)) === 
log(1 |> f(%) |> g(%))

and we are told to accept the reality of the spec of the hack-pipe, I firmly believe no one here can control the code with hack pipe any more.

How about this??

log( 1 |> log(f(%) |> g(%)) )

log( log(1 |> f(%) |> g(%)) )

or

log( 1 |> log(f(%) |> log(g(%))) )

log( log( log(1 |> f(%) |> g(%)) )

Since apparently, the grouping operator ( ) seems no longer to control the dependency graph of expressions under the Hack-pipe influences, regardless what's going on underneath, if you stands on the same ground

  • Grouping parentheses mean the same thing in Haskell as they do in high school mathematics. They group a sub-expression into a single term. This is also what they mean in JavaScript.

  • think of evaluation order as a totally separate concept than grouping.

I must maintain my own hypothesis Hack-style pipe operator |> takes precedence over grouping operator ( ) in order of operations in JavaScript

Although there's nothing stopping a language designer putting confusing nonsense into the rules of their language, and JavaScript is arguably not without its share already.

I agree with you in terms of this is merely putting confusing nonsense into JavaScript, please note this hack pipe proposal has just advanced to Stage-2.

JavaScript is arguably not without its share already.

Perhaps, but generally speaking, such a fact does not justify the extra confusion nonsense; especially for the substantial binary operator of function application f(x); for minimal/F# proposal is f(x) === x |> f, and the Hack proposal is not as we confirmed.


I will separate my answer responding to @Bergi.

Solution 5:

This is my own Answer after reading Answers by @Ben and @Bergi. My comments to respond to them under their Answers will not be sufficient in terms of the complexity and readability to readers, and editing the Question will also have the same problem for the amount of the document. (the previous duplicated section has been removed from the Question and moved here)

My response to Answer by @Bergi

Actually, this is the best explanation I've ever seen to illustrate what is the Hack pipe and how it works.


lhs |> rhs

is syntactic sugar for

(% => rhs)(lhs)

Very concise and great work! (The TC39-hack-pipe-proposal readme.md should add his explanation) I truly appreciate your great answer and sincerely will upvote it.

Having said that, here is my take regarding your insight and my own question - Does the Hack-style pipe operator |> take precedence over grouping operator ( ) in order of operations in JavaScript?

I believe now we share the common ground, and you say:

In particular, when you write 1 |> (log(f(%) |> g(%))); you cannot substitute the subexpression (log(f(%) |> g(%))) with 3.

Why not? Of course you have explained the reason I cannot substitute the subexpression, based on this; syntactic sugar System


lhs |> rhs

is syntactic sugar for

(% => rhs)(lhs)

and here is the tricky part. For some reason, you (@bergi) take this syntactic sugar System into account as a premise.

However, the fact is the syntactic sugar is a freehand creation, and there is no authority background. It's just an idea by someone.

On the other hand, my question is based on very common principle of mathematics.

  • Grouping parentheses mean the same thing in Haskell as they do in high school mathematics. They group a sub-expression into a single term. This is also what they mean in JavaScript.

  • think of evaluation order as a totally separate concept than grouping.

quoted from @Ben's answer: Does the functionality of Grouping operator () in JavaScript differ from Haskell or other programming languages?

In other words, my principle to use Grouping operator ( ) in JavaScript

The grouping operator ( ) controls the precedence of evaluation in expressions.

in the same manner as high-school-mathematics is refused by an idea of the syntactic sugar.

Observing such a situation, the question arises:

Does the Hack-style pipe operator |> take precedence over grouping operator ( ) in order of operations in JavaScript?

What do you think?

In fact, you say I have a confusion, and the fact is not. I'm not confused at all, and I think you are confused.

To me, if the creation of the system as the syntactic sugar is not compatible with the general usage of the grouping ( ), it's a failure work, and the community should not accept such a thing.

Post a Comment for "Does The Hack-style Pipe Operator |> Take Precedence Over Grouping Operator ( ) In Order Of Operations In Javascript?"