proposal-pipeline-operator: Inconsistency of Type of the operator

README.md of the current draft specification:

For F# proposal:

  • value |> x=> x.foo() for method calls,
  • value |> x=> x + 1 for arithmetic,
  • value |> x=> [x, 0] for array literals,
  • value |> x=> {foo: x} for object literals,
  • value |> x=> `${x}` for template literals,
  • value |> x=> new Foo(x) for constructing objects,
  • value |> x=> import(x) for calling function-like keywords,
  • etc.

The type is extremely simple and clear in terms of value |> f: where value: Object, Number, Array, Function or any others. f: Function (lambda expression)

In other words, the types == sets of the binary operation is well defined which fits to TypeScript eco.

In mathematics, a binary operation or dyadic operation is a calculation that combines two elements (called operands) to produce another element. More formally, a binary operation is an operation of arity two.

More specifically, a binary operation on a set is an operation whose two domains and the codomain are the same set.

Such binary operations may be called simply binary functions. Binary operations are the keystone of most algebraic structures that are studied in algebra, in particular in semigroups, monoids, groups, rings, fields, and vector spaces.

Binary operator/operation is identical to binary function:

image

x * y ===
operator(x, y) ===
operator(x)(y) 

or should we rewrite to:

a |> f ===
operator(a, f) ===
operator(a)(f) 

In fact, the conversion from binary function to binary operator happened in JS.

Fundamentally, essentially, this proposal is to introduce a new binary operator to JS, which is the same league of exponentiation operator ** introduced In ES2016 Syntax Math.pow(2, 3) == 2 ** 3 Math.pow(Math.pow(2, 3), 5) == 2 ** 3 ** 5 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation

Then, in this binary operator, the type is clear: Number ** Number

In any case, the input types == sets of x, y, or a , f should be defined (as we do in TypeScript) as a request from definition of function, and for F# proposal, a |> f, essentially it’s clearly defined in principle.


For Hack proposal:

  • value |> foo(^) for unary function calls,
  • value |> foo(1, ^) for n-ary function calls,
  • value |> ^.foo() for method calls,
  • value |> ^ + 1 for arithmetic,
  • value |> [^, 0] for array literals,
  • value |> {foo: ^} for object literals,
  • value |> `${^}` for template literals,
  • value |> new Foo(^) for constructing objects,
  • value |> await ^ for awaiting promises,
  • value |> (yield ^) for yielding generator values,
  • value |> import(^) for calling function-like keywords,
  • etc.

value: Object, Number, Array, Function or any others. The right side: ???

For instance,

  • value |> foo(1, ^) for n-ary function calls,

What is the type of foo(1, ^) ? Function?

https://github.com/tc39/proposal-pipeline-operator/issues/223#issuecomment-922914884,

You gave this example: value |> foo(1, ^) |> bar(2, ^) You can rewrite it as (value |> foo(1, ^)) |> bar(2, ^) or as value |> (foo(1, ^) |> bar(2, ^)) using hack pipes, and it will yield the same result. Isn’t this the definition of associativity?

So, to be clear:

value |> foo(1, ^) |> bar(2, ^) ==
(value |> foo(1, ^)) |> bar(2, ^)  ==
value |> (foo(1, ^) |> bar(2, ^))

and for your convenience, I can replace to: Well, I could replace to… IF I am allowed to have the type of foo(1, ^) as Function

a |> f |> g ==
(a |> f) |> g ==
a |> (f |> g)

So, now, what is f |> g? a |> f |> g ==a |> (f |> g) ?? Suddenly, the function application operator |> became to Function composition operator .? The type does not match.

For F# proposal, the above is, of course:

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

Associative in the form of Monad. https://github.com/tc39/proposal-pipeline-operator/issues/223#issuecomment-923324793

On the other hand, what is (f |> g) in the hack proposal? What is the (foo(1, ^) |> bar(2, ^))? What is the type?

According to https://github.com/tc39/proposal-pipeline-operator/issues/223#issuecomment-923225188 ,

It’s not a function at run-time, it’s not even legal syntax on its own. But if you really stretch the meaning, it is a “function” of the topic variable in the grammar — it can only appear on the RHS of pipe operator, which applies the “function” to the LHS.

So, suppose the type of (foo(1, ^) |> bar(2, ^)) is Function, Function |> Function = Function |> function application now behaves like . function composition.

Summary

With hack proposal pipe |>,

  1. foo(1, ^) is not an expression with the type of Function as it appears because typing it as Function, as I’ve illustrated the operator function-application becomes function-composition.  Impossible to type for this expression

  2. We will have an expression (foo(1, ^) |> bar(2, ^)) that is not even legal syntax on its own. Impossible to type for this expression.

  • value |> foo(1, ^) for n-ary function calls,
a |> ?? |> ?? == a |> (?? |> ??) == a |> ??

where ?? is something unknown that is impossible to type.

Now, probably the usage of TypeScript becomes useless even for “mainstream”.

|> has many contexts in terms of types, and something is impossible to type, and there is no way to tell in which context |> is used with full of ^ that also has context for each.

Finally, as a functional programmer who has used the pipe as f(x) = x |> f , since |> is strictly defined as the binary operator of between a value and function, it was easy to use and the code keeps robustness.

In fact, I appreciate feedback from TypeScript users with hack-pipe.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 58 (15 by maintainers)

Most upvoted comments

I am junior, and I will take up the courage to say a few words with my poor english.

After reading your writing, I understand hack pipe does not make sense in math, and I also can understand why ljharb says it make no difference. Because they admit that ?? |> ?? can not exist by itself, so I guess TS does not need give a type for ?? |> ?? ?

Suddenly, the function application operator |> became to Function composition operator . ?

As far as I know, it seems they did not say |> is function application operator or function composition operator ?

For me, it is just a operator for pipe nested operations in linear form, and I learned hack pipe is not function application operator, and it is also not function composition operator(because can not exist by itself).

And, it seems we can continue to try to add a real function application operator with a symbol |>>.

So, with all these information, what terrible things may hack pipe bring? Maybe it is the question which ljharb really care about?

At last, I personally can accept that ???? in below can not exist by itself and don’t have a type. For me, I just want an operator which can let me do a series of things with linear form.

   a |>  ?? |> ??  |> ??
== a |> (?? |> ??) |> ?? 
== a |>    ????    |> ??

As has been explained several times, the answer is 3. Refer to https://github.com/tc39/proposal-pipeline-operator/issues/227#issuecomment-926136875.

So, let’s reason about 1 |> (f(^) |> g(^)).

  1. 1 is evaluated
  2. 1 is pushed onto the ^ stack: [1]
  3. Evaluate the right hand side:
    1. f(^) is evaluated, ^ references the last item, so f(1), the result is 2
    2. 2 is pushed onto the stack: [1, 2]
    3. Evaluate the right hand side:
      1. g(^) is evaluated, ^ references last item, so g(2), the result is 3.
      2. return 3
    4. pop ^ stack: [1]
    5. return 3
  4. pop ^ stack: []
  5. return 3

“But wait!” you ask, “(f(^) |> g(^)) has to evaluate first! Gotcha!”. It did evaluate first, and that’s the part you don’t seem to understand. There’s a difference between nested evaluations and sequential evaluations.

In the case of (1 |> f(^)) |> g(^), what happens? You have sequential evaluations of the two different pipes, not a nested evaluation.

  1. 1 is evaluated
  2. 1 is pushed onto the ^ stack: [1]
  3. Evaluate the right hand side:
    1. f(^) is evaluated, ^ references the last item, so f(1), the result is 2
    2. return 2
  4. pop ^ stack: []
  5. (At this point, 1 |> f(^) is finished evaluating and its result is 2. We’re evaluating 2 |> g(^). This is the second evaluation of the pipe, we’re at a brand new expression.)
  6. 2 is evaluated
  7. 2 is pushed onto the ^ stack: [2]
  8. Evaluate the right hand side:
    1. g(^) is evaluated, ^ references last item, so g(2), the result is 3.
    2. return 3
  9. pop ^ stack: []
  10. return 3

So, now of course you’re gonna be pedantic and demand I answer what 1 |> f(^) |> g(^) does. Well, it’s the exact same as the first case. Why? Because the spec is defined that way. Instead of relentlessly quoting your math ideas in this thread, please read the spec.

And if you’re not willing to do so, then you’ll need to trust us when we give you authoritative answers. Because we can read the spec.

I have provided a concrete full code. https://github.com/tc39/proposal-pipeline-operator/issues/227#issuecomment-926237799 https://github.com/tc39/proposal-pipeline-operator/issues/227#issuecomment-926248041

and this is the third time:

const f = a => a * 2;
const g = a => a + 1;

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

Now we made (f(^) |> g(^)) to be evaluated before other expressions with higher priority. What is the evaluated value? where

Grouping operator ( )

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

The grouping operator consists of a pair of parentheses around an expression or sub-expression to override the normal operator precedence so that expressions with lower precedence can be evaluated before an expression with higher priority.

Please answer.

https://github.com/tc39/proposal-pipeline-operator/issues/229#issuecomment-926308352 @tabatkins

So now, a member admitted that the current proposal override the Grouping operator ( ) with Hack |>

The new operator with the highest precedence ever in JavaScript. I wonder how the JS community react… because basically breaking the () functionality is against mathematics, too.

@stken2050: At this point, 3 people have tried to give you the correct answer. Let’s walk through this slowly:

function two(x) {
  console.log('two');
  return 2;
}
function three(x) {
  console.log('three');
  return 3;
}
function four(x) {
  console.log('four');
  return 4;
}

console.log(two() * (three() + four()));

This logs:

  1. "two"
  2. "three"
  3. "four"
  4. 14

Notice the order "two", "three", "four" isn’t affected by the parenthesis. Let’s try (two() * three()) + four(). Surprise, it’s still "two", "three", "four"! But now the final log is 10. That’s because the operands are evaluated left to right, and this doesn’t change. What changed was the evaluation of the operators. I’m certain, because you’re so good with math, you can reason why 2 * (3 + 4) is different than (2 * 3) + 4.

Now you are saying the evaluation of function is only operator and x is nothing to do with it.

That’s not what I said.

Anyway, your copy & pasting walls of text over and over doesn’t help with anything. Your question what is the value of 1 |> (f(^) |> g(^)); has been answered long before, I don’t know why you’re reposting it. It’s 3.

It seems to me you simply refuse to acknowledge that (f(^) |> g(^)) does not have a value, because it’s not a valid Expression, it’s only valid as PipeBody on the right-hand-side of |> and needs to be fed topic to be evaluated.

It’s not math, it’s JavaScript. The core baseline we must always start with is existing JavaScript semantics. Anything else is only relevant when it doesn’t conflict with that.

Hack |> overrides the Grouping operator ().

It doesn’t. Parentheses work as always, but thanks to associativity, rearranging them doesn’t change the result. In that sense, |> is a well-behaved operator unlike say +, which does not guarantee that a + (b + c) === (a + b) + c.

(expr |> foo(^)) |> bar(^);
// evaluation order is left to right:
// 1. _expr = expr
// 2. _foo = foo
// 3. _foo_expr = _foo(_expr)
// 4. _bar = bar
// 5. _result = _bar(_foo_expr)
expr |> (foo(^) |> bar(^));
// evaluation order is left to right:
// 1. _expr = expr
// 2. _foo = foo
// 3. _foo_expr = _foo(_expr)
// 4. _bar = bar
// 5. _result = _bar(_foo_expr)

@xxleyi Thanks for your courage to speak up.

After reading your writing, I understand hack pipe does not make sense in math

Correct, it does not make sense in mathematics if seriously investigated that is what I’ve done last week, and I firmly believe such a thing should not be accepted for the very basic function application operator because I think this one will severely mess up the Algebra structure of the entire JS eco.

Secondly, currently, it appears either of the below: a) The advocators of Hack never accept the fact Hack pipe breaks very basic laws in Algebra. b) Once accepted, the advocators insist it’s not the “goal” of this proposal.

For a) I’ve encountered this problem a lot until now, however, eventually, the fact will be revealed to the public in any way because this is math. It’s relatively easy to prove, and this one is not politics.

For b) some political matter to be discussed, however, I don’t think the majority of JS will happily accept a new binary operator in principle that has algebraic consistency but actually inconsistent.

In fact, according to #StateOfJS 2020: What do you feel is currently missing from JavaScript? https://2020.stateofjs.com/en-US/opinions/missing_from_js

image

It’s obvious that majorities have longed for more strictness of JavaScript

  • Static Typing
  • Pattern Matching
  • Pipe Operator
  • functions
  • Immutable Data Structure

image

- Static Typing

the thing super majorities feel currently missing from JavaScript.

Therefore, I expect the messed-up-typed-hack-pipe will not be accepted by them.

At last, I personally can accept that ??? in below can not exist by itself and don’t have a type. For me, I just want an operator which can let me do a series of things with linear form.

According to the research above, such an opinion is against the trend of the needs in JS, or any other languages. The majority needs more strict typed environment that helps their coding to be robust.

Surly, TypeScript community will easily discover for this hack |>, things are impossible to type. It’s as a matter of course because the designers have not paid any attention to the algebraic strictness, they simply ignored it because it’s not the goal of this proposal. Pity.

As far as I know, it seems they did not say |> is function application operator or function composition operator?

As reading readme.md, I have a strong impression that the proposal is camouflaged as if the difference is very little but the fact is the difference is significant.

It’s obvious when the majority of JavaScript long for

  • Pipe Operator

which must be the function application, of course, but actually, this one is not.

The problem is in read-me or in other places, as far as I know, they never clarified this aspect to the public broadly, which is why I have not noticed and I think the majorities still believe what’s coming is F#-style pipe.

And, it seems we can continue to try to add a real function application operator with a symbol |>>.

https://github.com/tc39/proposal-pipeline-operator/issues/225#issuecomment-924479581 A couple of people including me start thinking that hack-pipe is so harmful that if it’s the default route as they claim, it’s far better not to have any. I feel very sorry not to have F# or minimal style, but it’s better than JS will be broken forever. Then, I also hope we have operator overloading, then totally no problem.

Here the parentheses make sure the |> operator above B is evaluated before the |> operator above A.

operator is evaluated with operands. As I said earlier, binary operator is a syntax sugar of binary function.

In binary operation, the function application is operator(LHS, RHS)

Now you are saying the evaluation of function is only operator and x is nothing to do with it.

Please stop inventing your own math rule.

In mathematics, a binary operation or dyadic operation is a calculation that combines two elements (called operands) to produce another element. More formally, a binary operation is an operation of arity two.

More specifically, a binary operation on a set is an operation whose two domains and the codomain are the same set.

Such binary operations may be called simply binary functions. Binary operations are the keystone of most algebraic structures that are studied in algebra, in particular in semigroups, monoids, groups, rings, fields, and vector spaces.

Binary operator/operation is identical to binary function:

image

x * y ===
operator(x, y) ===
operator(x)(y) 

or should we rewrite to:

a |> f ===
operator(a, f) ===
operator(a)(f) 

In fact, the conversion from binary function to binary operator happened in JS.

Fundamentally, essentially, this proposal is to introduce a new binary operator to JS, which is the same league of exponentiation operator ** introduced In ES2016 Syntax Math.pow(2, 3) == 2 ** 3 Math.pow(Math.pow(2, 3), 5) == 2 ** 3 ** 5 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation

Then, in this binary operator, the type is clear: Number ** Number

Please stop inventing your own math rule.

You seem to not understand what you really are doing.

Do you guys override the Grouping operator ( ) with Hack |> ?

That is not how JS works. In a(1) + (b(2) + c(3)), the function calls are evaluated in the order they’re written. Same with (a(1) + b(2)) + c(3). Same with a(1) ** (b(2) ** c(3)) and with a(1) |> (b(^) |> c(^)). The first thing evaluated will always be a(1) no matter where you place parentheses.

OT: to clarify what I meant before with + not being associative, that’s due to IEEE floating-point arithmetic. 1e16 + (1 + 1) !== (1e16 + 1) + 1

@stken2050 but that’s not what the grouping operator does in every case, so I don’t think your expectation holds. Parens don’t change left-to-right evaluation.

It’d be 3 in both cases. “before the other expression with higher priority” is not what grouping with parens does, and in this case, I believe pipeline is both left and right associative, so parens have no effect on the result - which is true in a * b * c whether you group the first two or last two operands.

what part of “controls the precedence” is a law that’s been broken?

Furthermore, how can we know the Babel REPL that you currently use is the bug-free and the TypeScript PR behavior originated from the bug you claim? What is your reasonable standard?

The Babel implementation has been done by @js-choi (one of the champions of the Hack pipes proposal). The “reasonable stardard” is the spec proposal: either you learn how to read it (I’d be happy to help, if you are confused about any spec part), or you trust what who can read it says.

We can follow the spec (https://tc39.es/proposal-pipeline-operator) step by step to see how value |> (foo(1, ^) |> bar(2, ^)) is evaluated:

  1. When the JS interpreter finds the outer pipe expression, it: (https://tc39.es/proposal-pipeline-operator/#sec-pipe-operator-runtime-semantics-evaluation)
    1. Evaluates value
    2. Calls EvaluateWithTopics (https://tc39.es/proposal-pipeline-operator/#sec-evaluatewithtopics) on (foo(1, ^) |> bar(2, ^)), which:
      1. 🍌 (step 3) clones the current lexical environment (where variables are stored), and sets the new environment’s topic values to the topicValues spec variable, which is a list containing a single value (cc @js-choi this doesn’t need to be a list, it can be simplified): it contains the result of evaluating value
      2. (step 6) evaluates foo(1, ^) |> bar(2, ^)

When evaluating foo(1, ^) |> bar(2, ^), it: (https://tc39.es/proposal-pipeline-operator/#sec-pipe-operator-runtime-semantics-evaluation, again)

  1. Evaluates foo(1, ^)
    1. When evaluating it, it needs to evaluate foo, then 1, and then ^. When evaluating ^ calls GetPrimaryTopicValue (https://tc39.es/proposal-pipeline-operator/#sec-topic-references-runtime-semantics-evaluation), which:
      1. (step 1) gets the first lexical environment which has a topic binding. Since the topic binding was defined at step 🍌 above has a topic binding, it’s this environment.
      2. The result of evaluating ^ is the first (and only) topic value associated to that environment, so it was the result we got previously while evaluating value.
  2. It then calls EvaluateWithTopics (https://tc39.es/proposal-pipeline-operator/#sec-evaluatewithtopics) on bar(2, ^), creating a new environment whose ^ binding is the result got previously while evaluating foo(1, ^).