Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 JavaScript


Pipe Operator (|>) for JavaScript

  • Stage: 2
  • Specification
  • Babel plugin: Implemented in v7.15. See Babel documentation.

There were two competing proposals for the pipe operator: Hack pipes and F# pipes.


The minimal/F#-style pipe-operator is just a binary operator of function application between a value x and a function f in algebraic sense that is:

f(x) === x |> f

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

As this is a simple replacement of the mathematical expressions, there is nothing to relearn and so-called referential transparency is guaranteed.

Referential transparency and referential opacity are properties of parts of computer programs. An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program's behavior.


Now, they have chosen the Hack-style pipe advanced to TC39 Stage2.


Pro: The righthand side can be any expression, and the placeholder can go anywhere any normal variable identifier could go, so we can pipe to any code we want without any special rules:

  • value |> foo(^) for unary function calls,
  • value |> foo(1, ^) for n-ary function calls,
  • value |> ^.foo() for method calls,
  • value |> ^ + 1 for arithmetic,
  • etc.

Although the Hack-pipe proposal team claims as

Pro: The righthand side can be any expression

this means the type of |> is no longer as simple as the type of minimal/F#-style pipe-operator:

  • x : Object
  • f : Function

Therefore, I need to investigate what's really going on underneath using Babel: Implemented in v7.15.


Test-1

REPL with an example code with a configuration

Image: OPTIONS - Pipeline proposal - Hack

const f = a => a * 2;
const g = a => a + 1;
 
1 |> f(%) |> g(%);
1 |> (f(%) |> g(%));

transpiled to

Image of Babel transpile

var _ref, _ref2, _ref3, _ref4;
const f = a => a * 2;
const g = a => a + 1;

_ref2 = 1, (_ref = f(_ref2), g(_ref));
_ref4 = 1, (_ref3 = f(_ref4), g(_ref3));

which indicates

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

the both expressions share an identical structure under the Hack-pipe.

(I have confirmed this result is on-spec and expected from one of the champion team of Hack-pipe proposal)

In the principle of Grouping operator ( ) in JavaScript, this should be invalid.

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

The grouping () rules the mathematical structure (dependency graph) of expressions.

In mathematics, computer science and digital electronics, a dependency graph is a directed graph representing dependencies of several objects towards each other. It is possible to derive an evaluation order or the absence of an evaluation order that respects the given dependencies from the dependency graph.

Surely, there is a factor of evaluation order by the evaluation strategy (eager evaluation for JavaScript), however, the algebraic structure (dependency graph) should be changed accordingly,

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

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

and the transpile fact shows the Hack-pipe ignores the principle.

See the great answer for Does the functionality of Grouping operator () in JavaScript differ from Haskell or other programming languages?


Test-2

So, If the Hack-pipe follows the rule of the Grouping operator in JavaScript, or any other programming languages, for the expression:

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

regardless of the evaluation order, the dependency graph should be:

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

Now I have a log function to show the value.

const right = a => b => b;
const log = a => right(console.log(a))(a);

This behaves like identity function: a => a which does not affect to the original code but console.log(a) in the process.

Now, we want to know the evaluated value of (f(%) |> g(%))

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

which transpiled to

Image of Babel transpile

and the console.log result is 3 regardless of the order of the evaluation.

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

where

const f = a => a * 2;
const g = a => a + 1;
1 |> f(%) |> g(%);    // 1 * 2 + 1 = 3
1 |> (f(%) |> g(%));  // 1 * 2 + 1 = 3 with hack-pipe

therefore,

1 |> 3 == 3

which indicates Hack-pipe is logically broken and does not make sense any more to code.


My question, or what I would like you to distinguish is:

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

Objective answers not subjective/opinion base, please. Thanks.



For comments:


1.

We share the fact that the Grouping operator has the highest precedence according to Operator precedence.

enter image description here


2.

According to the tc39/proposal-pipeline-operator Hack proposal

enter image description here

Yes, this is a claim by a member of the champion team of the Hack pipe proposal.

Now, the claim is against the fact that I examined here, and that is why I want you distinguish, including what I miss in my investigation here.


The purpose of the question is to know the fact of the hack-pipe eliminating obscurity in interpretation or ignoring the exact implementation underneath.

like image 712
KenSmooth Avatar asked Dec 19 '25 16:12

KenSmooth


1 Answers

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 me1. 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.


1 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.

like image 92
Ben Avatar answered Dec 21 '25 08:12

Ben



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!