Skip to content

Pipelines with operator overloading #228

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

Closed
runarberg opened this issue Sep 22, 2021 · 24 comments
Closed

Pipelines with operator overloading #228

runarberg opened this issue Sep 22, 2021 · 24 comments
Labels
follow-on proposal Discussion about a future follow-on proposal

Comments

@runarberg
Copy link

It might be worth to consider that if the Operator Overloading proposal advances with the pipeline operator, that creating pipelines by overloading an operator with a Function on the RHS will be trivial. An implementation might look something like this (from #206 (comment)):

const PipeOps = Operators(
  {},
  {
    right: Function,
    "|"({ value }, fn) { return new Pipe(fn(value)); },
  },
);

class Pipe extends PipeOps {
  value;
  constructor(value) {
    super();
    this.value = value;
  }
}

with operators from Pipe;

const double = (n) => n * 2;
const addTwo = (n) => n + 2;

const { value } = new Pipe(20) | double | addTwo
console.log(value);
// => 42

Given the popularity of libraries that encourage point free pipelines (such as RxJS and fp-ts) I would assume library authors would take advantage of this and overload the bitwise OR operator to provide a point free pipeline API for use with their libraries. The situation might occur that we end up with two paradigms for creating pipelines. One that is included in the language with a special operator |> and another provided by various libraries |. This situation might be confusing to users that are used to one pipeline (say hack style) but not the other, as the two kinds of pipelines are quite similar (both visually and functionally) but still quite distinct. For example given a developer who is used to write something like this:

envars
  |> Object.keys(^)
  |> ^.map(envar => `${envar}=${envars[envar]}`)
  |> ^.join(' ')
  |> `$ ${^}`
  |> chalk.dim(^, 'node', args.join(' '))
  |> console.log(^);

Then goes on to see something like this:

import Pipe from "my-pipe-lib";
import { range, filter, map, take, reduce } from "my-iter-lib/piped";
with operators from Pipe;

const { value } = new Pipe(
  range(0, Number.POSITIVE_INFINITY),
)
  | filter(isPrime)
  | map((n) => n * 2)
  | take(5)
  | reduce((sum, n) => sum + n, 0);

In this hypothetical scenario the developer might be quite familiar with how pipelines work but the latter pipelines is quite unlike the pipelines they are used to, and might be a bit confusing. It might even feel a bit like magic to write the pipelines without the topic marker. They will probably have to go to the library’s documentation and hope the difference between their overloaded operator pipeline and the pipelines that are already in the language is adequately explained (see #225 (comment)).

So there are a few questions that arise here:

  • Do we need the pipeline operator at all if libraries can provide it with operator overloading?
  • If so, what is gained by the special |> pipeline over overloaded operators provided from libraries?

The following questions might apply more in the operator overloading proposal:

  • Can we avoid common footguns with user defined pipeline operators such as:
    • Unbound functions (| console.log vs. | console.log.bind(console); potentially avoided with the partial application proposal).
    • Async steps.
    • non-function RHS.
  • How hard will it be for library authors to explain the difference between the language included pipe operator |> and their user defined pipe operator |?
@nicolo-ribaudo
Copy link
Member

What would be the advantage of using operator overloading rather than just using a pipe function, since you have to write new Pipe anyway?

@runarberg
Copy link
Author

runarberg commented Sep 22, 2021

The most obvious for me is that the pipe operator | is a lot easier to type then the pipe function (see #219). In TypeScript the operator could look something like this:

const PipeOps = Operators(
  {},
  {
    right: Function,
    "|"<T, U>({ value }: Pipe<T>, fn: (value: T) => U): Pipe<U> {
      return new Pipe(fn(value));
    },
  },
);

class Pipe<T> extends PipeOps {
  value: T;
  constructor(value: T) {
    super();
    this.value = value;
  }
}

Another reason might be that the order of operation might matter. The overloaded operator | would operate in the same order as the pipeline operator |>, which is not the same as the pipe function (see #202 (comment)). The pipe function would first compose all the functions together and then evaluate the composed function, whereas the pipe operator | would evaluate the each function with the value from the previous step. This might matter although I don’t know of any actually examples in real code.

As for having to have to write new Pipe anyway, I would imagine that would be the case with libraries such as fp-ts, but not for RxJS. I would imagine that libraries like RxJS would implement the operator overloading straight onto their constructors.

import { Observable, fromEvent, throttle, filter, map, forEach } from "my-observable-lib";
with operators from Observable;

fromEvent("keyDown", element)
  | filter((event) => event.key === "Enter")
  | throttle(500)
  | map((event) => event.target.value)
  | forEach((value) => console.log(value));

@ken-okabe
Copy link

ken-okabe commented Sep 22, 2021

@runarberg

In this hypothetical scenario the developer might be quite familiar with how pipelines work but the latter pipelines is quite unlike the pipelines they are used to, and might be a bit confusing.

Good point.
According to Why a pipe operator of README.md,

In the State of JS 2020 survey, the fourth top answer to
“What do you feel is currently missing from
JavaScript?”

was a pipe operator.

image

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

It's reasonable to observe that the majority of the JS community has longed for more strictness of the language.

Especially for Static Typing,

image

An overwhelming preference is presented as Typing strictness over the rough coding in JS, and as I've illustrated earlier, there is Inconsistency of Type of the operator #227 in the current proposal (hack-style), and I firmly believe this style that is impossible to type will never be accepted to the current community who want the more strict feature of the type system, and without the native implement, they use TypeScript so far.

The pipe operator in the survey must be in the context of Static Typing with the functional property as other ones such as Pattern Matching (the third), functions(the fifth), and Immutable Data Structures(the seventh).

Therefore, what's they believe coming with a lot of expectations is a functional pipeline operator such as your code:

fromEvent("keyDown", element)
  | filter((event) => event.key === "Enter")
  | throttle(500)
  | map((event) => event.target.value)
  | forEach((value) => console.log(value));

So there is confusion especially, if we have the hack-style first, then F#-style later.

Do we need the pipeline operator at all if libraries can provide it with operator overloading?

No, for many including me, I observe. I can edit later, but I've found some of the members here claim it's far better not to have any pipe than to have hack-style because they evaluated the one harmful to JavaScript.

#205 (comment) @jderochervlk

The future of functional programming is Hack pipe

Nope. It isn't. If this is truly what became added to the language I would continue to use Ramda's pipe, which is a shame because I would really love to remove some overhead of installing a package in order to do something so simple.

#205 (comment) @samhh

I'm thinking ahead. JavaScript won't cease to exist once Hack reaches stage 4.

#215 (comment) @arendjr

I would rather have the language remain without any pipe operator than to have to deal with Hack in the future.
I too would like to thank @js-choi and the other contributors for all their effort, but I believe the current direction to be misguided to the detriment of not just the FP community, but the JS community at large.

#225 (comment) @voronoipotato

The argument against hack pipes is that we believe they are a hazard (sometimes for different reasons, but the conclusion is the same). Most of us in this thread and I suspect in the wild, would rather have no pipes, than hack pipes.

#225 (comment) @SRachamim

PFA proposal is a universal solution to those who worries about the arrow function noise on all three cases: map, then and pipe. If PFA is not ready, and we don't want minimal/F# without PFA, then let's wait, instead of introducing an irreversible Hack pipes.

#225 (comment) @SRachamim

And as I said, if PFA is stuck, then it's not a good reason to introduce Hack. We should either wait, or avoid pipe at all (or introduce minimal/f# style anyway).

#225 (comment) @Lokua

Also, as a writer whose job is to communicate meaning, hack style removes my ability to provide descriptive names, which is basically the universal first step of writing readable code. I'm honestly shocked at this proposal. Imagine if you started at a new company and they enforced that all unary functions you write regardless of context had to name their single argument x (or god forbid, ^ :trollface:). That's how I feel and I'm not exaggerating - this proposal scares me because it's going to lead to code that is uglier, harder to read, and harder to refactor/abstract later on!
Edit: I too would rather see no pipe than this and am admittedly biased as I've always thought a pipe operator (or pretty much any more new syntax) is a bad idea. But if we are going to have it, I'd rather see it implemented in a way that improves readability, not (IMHO) hinders it.

Then, considering @js-choi 's great article in Prior difficulty persuading TC39 about F# pipes and PFA #221 and Brief history of the JavaScript pipe operator, the option to discard this proposal seems reasonable to me, and instead, we will obtain operator-overloading in the white canvas.

This is an operator for function application, but I do also need function composition because they are both sides of the same coin in Algebra.

image

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

Also, we will need Monad operators, actually, the operator for function application |> and function composition . is Monad, but still there are infinite binary operators in theory, and with operator overloading, it is possible to obtain any.

So a very reasonable question will be why only the binary operator for function application must be Hacked??

My answer would be unfortunately It happened that the function application operator was chosen among other operators, then tweaked with no respect of Math, and it's fine as long as it was a statement design, but actually a binary operator.

As a consequence, it becomes something that is impossible to type and impossible to combine with function composition that is another side of the same coin, which is the fundamental reason for the incompatibility of point-free-style programming.

#225 (comment) @runarberg

Given that both hack pipelines and operator overloading might be redundant in giving us the ability to pipe values into functions, it is often wise to pick the one which has more broad use-cases. It can be debated which operator that is F# pipes or Hack pipes, but it obvious that operator overloading beats hack pipelines in broadness of use-cases.

it is often wise to pick the one which has more broad use-cases.

Wisdom of abstraction that we should know in software development.

With operator overloading, that is the abstraction of an infinite number of (binary or other) operators, we can avoid the current issues.

@mAAdhaTTah
Copy link
Collaborator

I'm sympathetic to the RxJS example because it's narrowly scoped to be used with Observable and does address a use case not fully covered by Hack pipe. However, I don't understand why this:

const { value } = new Pipe(20) | double | addTwo

is preferable to this:

const value = 20 |> double(^) |> addTo(^)

The latter is significant shorter, clearer, doesn't require allocations of new closures and objects for every step, and doesn't require an extra destructuring + potential destructuring rename to get a useful variable. If you wanted to write the former as a function, it has to either be this:

const doubleThenAddTwo = x => {
  const { value } = new Pipe(20) | double | addTwo
  return value;
}

Or this:

const doubleThenAddTwo = x => (new Pipe(20) | double | addTwo).value

So in regards to your first question:

Do we need the pipeline operator at all if libraries can provide it with operator overloading?

I think it would be significantly advantageous to have this in the language proper compared to sideloading it via operator overloading.

@noppa
Copy link
Contributor

noppa commented Sep 22, 2021

In this hypothetical scenario the developer might be quite familiar with how pipelines work but the latter pipelines is quite unlike the pipelines they are used to, and might be a bit confusing. It might even feel a bit like magic to write the pipelines without the topic marker.

In this hypothetical scenario, wouldn't it be more likely that they are actually confused by the use of bitwise OR operator in this magical way, not by the resemblance to some other operator?

Operator overloading on its own already sets the bar pretty high for the reader. If they get confused by two different but similar-looking operators working slightly differently, oh boy will they have a bad time reading code with the exact same operators repurposed to do many completely different things.

Given the popularity of libraries that encourage point free pipelines (such as RxJS and fp-ts) I would assume library authors would take advantage of this and overload the bitwise OR operator to provide a point free pipeline API for use with their libraries.

Have any of the authors of these libraries indicated that they'd be interested in incorporating operator overloading if it came to pass? And if so, that | would be their choice of the operator and not, say, >>?

@runarberg
Copy link
Author

In this hypothetical scenario the developer might be quite familiar with how pipelines work but the latter pipelines is quite unlike the pipelines they are used to, and might be a bit confusing. It might even feel a bit like magic to write the pipelines without the topic marker. — @runarberg

In this hypothetical scenario, wouldn't it be more likely that they are actually confused by the use of bitwise OR operator in this magical way, not by the resemblance to some other operator? — @noppa

I don’t think so. Developers are smart, and usually are able to determine meaning by the surrounding context. The bitwise OR operation is pretty much only used in calculating bitmasks or while doing extreme optimization. In both cases it is easy to see it from the context.

Operator overloading on its own already sets the bar pretty high for the reader. If they get confused by two different but similar-looking operators working slightly differently, oh boy will they have a bad time reading code with the exact same operators repurposed to do many completely different things. — @noppa

I don’t think the main problem is that | is visually similar to |> but that pipelies using either are functionally similar. I think what trips most developers is when things are functionally very similar but distinct in subtle. I.e a developer can easily see that this is supposed to be read as a pipeline but not as a series of bitwise OR operations:

const { value } = new Pipe(
  range(0, Number.POSITIVE_INFINITY),
)
  | filter(isPrime)
  | map((n) => n * 2)
  | take(5)
  | reduce((sum, n) => sum + n, 0);

The same would hold true regardless of which operator were overloaded within reason (i.e. be it |, >>, or ^, but not something crazy like +, -, or ==). The confusion arises because: a) we use a different operator for this pipeline then what I’m used to, and b) we are calling functions in each step without using the topic marker. If the |> operator wasn’t in the language both of these confusions wouldn’t arise in the first place and it would be enough to explain the operator overloading it self without getting into how this pipeline is different from the |> pipeline.

Have any of the authors of these libraries indicated that they'd be interested in incorporating operator overloading if it came to pass? — @noppa

No I haven’t but if any of them likes to comment, please do so (cc. @benlesh). However if I was an author of a popular library I wouldn’t start considering this before Operator overloading reaches stage 2. That said, I am also thinking about the libraries of tomorrow. I personally would be tempted to provide an API such as this if I saw it fit for the construct I was providing. And if that pattern were to become popular, JavaScript would then have this added weirdness of two similar operators but distinct in subtle ways.

@runarberg
Copy link
Author

runarberg commented Sep 22, 2021

@mAAdhaTTah This example:

const { value } = new Pipe(20) | double | addTwo

Was only used to provide a usage example of how the defined operator overload would behave when used, not to prove it’s UX superiority over the hack pipes. I do that with other examples further below.

@ken-okabe
Copy link

ken-okabe commented Sep 22, 2021

https://github.com/tc39/proposal-operator-overloading/blob/master/README.md#usage-documentation

  • Bitwise operators: unary ~; binary &, ^, |, <<, >>, >>>
  • With future proposals, |>, ?., ?.[, ?.(, ?? (based on function calls, property access, and checks against the specific null/undefined values, so similar to the above)

Bitwise operators seems to have potential for overload.

@mAAdhaTTah
Copy link
Collaborator

Was only used to provide a usage example of how the defined operator overload would behave when used, not to prove it’s UX superiority over the hack pipes. I do that with other examples further below.

Fair enough – the RxJS case (and generally "importable methods") would be the case I'd be supportive of using operator overloading to solve. I don't think generic function pipelining is a compelling enough case, which suggests to me that operator overloading & Hack pipe could live alongside each other.

@shuckster
Copy link

The latter is significant shorter, clearer

It is? I know we're not in the "readability" thread, but I must say that the destructuring here is not adding to the cognitive load for me at least, since in both cases my eye was drawn first to the RHS of each:

new Pipe(20) | double | addTwo

20 |> double(^) |> addTo(^)

I simply cannot get my head around the fact that you'd consider the latter easier to read than the former, other than taking your word for it. I'm sure you do find the latter easier to read - I'm not saying otherwise - except to say that perhaps the "readability" argument isn't quite as clear-cut as we both might think?

doesn't require allocations of new closures and objects for every step

I read with appreciation your detailed reply to some of my own speculations on performance a few days ago. In that reply you said this:

I'm walking through all of this not because I care deeply about performance.

Now forgive me, but I am taking this out of context of course in order to press the point, but bear with me. I think the reply was well considered and made me think a lot. But I have to wonder what exactly you mean by performance? More specifically, when do you mean it?

I'm going to try and leave Moore's Law out of it this time since that didn't seem to land, and I'll just say that I do concede the point that allocating a closure exhibits different performance to not allocating a closure. But can we admit that day-to-day decisions between doing things one way or another are often driven by readability at the expense of performance? We do it all the time in trivial ways: slicing up our programs up into so many functions just for the sake of organisation, for the sake of helping our future selves read the blessed things.

When it comes to real-world experience, I can't tell you how much React code I've read that is utterly hammering the call-stack with Heisen-closures. But it doesn't really seem to matter most of the time, and when it does the offending factors usually go far deeper than just "capturing too many closures." It's mainly down to leading by example and educating each other with good practices and trying to keep on top of this, and this is beyond the scope of a single language feature.

Anyway, this brings me to paraphrase what you very eloquently stated in the other thread -- that we would do well to consider the performance characteristics of the features of a programming language holistically. Of course, we also do this in our day-to-day as users of languages. We make trade-offs on time-to-deliver vs. maintainability, with performance usually taking the hit, because we all very well know the mantra about premature optimisation.

So we all quite happily keep to using map/reduce/filter and never deign to bother unrolling them into for-loops, except as a last-resort, or if we're library authors looking to squeeze something out of our 10th refactor. But even when we say we care about perf, we say it while we happily use crazy things like React, Immer, RxJS or whatever, all in combination, just so we can get a team to rally behind some standard ways of doing things. By and large it works pretty well, but of course it could be always be better!

Anyway, to lurch this horrible wall of text back on topic, I would say that operator overloading is completely anathema to whatever version of |> a developer might side with. On the subject of performance, it seems that ceiling is far lower than for a dedicated pipe-operator. On the point of readability, interop is pretty much guaranteed with either Hack or F#. But without a standardised pipe-operator already in place before op-overloading lands, who knows what rough custom-pipelining beasts will slouch their way towards Bethlehem to be born? 😬

Or in other words, I completely agree with:

I think it would be significantly advantageous to have this in the language proper compared to sideloading it via operator overloading.

(But yeah, let's make it the F# one please. Limitation begets creativity. 😁 )

@ken-okabe
Copy link

new Pipe(20) | double | addTwo
20 |> double(^) |> addTo(^)

When we talk about operator overload in language level, it should be the comparison between

20 | double | addTwo
20 |> double(^) |> addTo(^)

where | is overloaded as minimal style pipe.

@benlesh
Copy link

benlesh commented Sep 23, 2021

I've been told several influential members want to discourage point-free programming. Therefore, anything that might help functional programming libraries is unlikely to pass the TC39. It is what it is. It's the hack proposal or nothing. IMO, the hack proposal isn't useful enough to justify the additional syntax. But I'm not on the committee

Please don't @ me into these threads. I've said my piece. Pretty thoroughly. I wasn't really heard. And I lost a friend over it. I simply just don't care what happens with this anymore. If it passes, great. Unfortunately, I don't really have any use for the proposed pipeline operator. But I'm hopeful for other features someday that I will have a use for.

@ken-okabe
Copy link

ken-okabe commented Sep 23, 2021

I feel terribly sorry for the matter. Really..

IMO, the hack proposal isn't useful enough to justify the additional syntax. But I'm not on the committee

I don't really have any use for the proposed pipeline operator.

There are many people here who share his thoughts.

It's the hack proposal or nothing.

If it would be a choice of poison or nothing, We want to chose nothing and wait for other features such as operator overloading.

@mAAdhaTTah
Copy link
Collaborator

I'm going to respond the readability comments in the readability thread (#225).


On the performance front, I do need to provide some context. The genesis of that conversation was a concern that was raised by an engine implementor as to the performance characteristics of F# pipes. I wasn't arguing about performance because I particularly care about performance, as I said in that quote, but I was defending that as a valid concern of theirs. Two things about this:

  1. We'll need engine implementors on board in order to actually, you know, implement it. If they have concerns, those concerns need to be actively addressed or they may not implement the feature (see TCO). There's no way around this, so
  2. Engine implementors are functioning at the scale of not only the entire web, but Node/Deno and whatever other runtimes use that engine. At that scale, things that may seem trivial to can have significant ramifications for them. They may very well implement & test and find that it's not a big deal, but at that scale, I have no idea how they weigh those trade-offs, so its a risk that they find the perf characteristics unpalatable.

As I said, I don't really feel strongly about performance. and when I advocated for F#, I was usually dismissive of perf concerns because I didn't think they mattered. And I still basically don't, except insofar as they matter to engine implementors. All I was trying to do in that quote was articulate their concerns, address some of the language comparisons, and use that to make a separate point about language characteristics & how they interact with each other.

On the subject of performance, it seems that ceiling is far lower than for a dedicated pipe-operator.

This is true and I think that means the value operator overloading brings to the language has a higher bar to clear. If you want pipe, don't hitch your wagon to that.


Tiny thing to close:

I must say that the destructuring here is not adding to the cognitive load for me at least, since in both cases my eye was drawn first to the RHS of each:

Destructuring would add to the cognitive load if you had to rename:

const { value: total } = new Pipe(20) | double | addTwo

@ken-okabe
Copy link

ken-okabe commented Sep 23, 2021

Performance issue again?
We've heard that pipe-line operator itself has a performance issue, and hack-style has less performance issue, and now you say

the value operator overloading brings to the language has a higher bar to clear. If you want pipe, don't hitch your wagon to that.

No, we don't want pipe any more. It's redundant over the operator overloading. We no longer support this pipeline-operator proposal as a whole.

And I believe the performance issue of operator overloading is off-topic here. Please refrain from taking advantage of performance issues for every discussion. Thanks.

PS. FYI, Rust language has Operator Overloading and it's fast.
https://doc.rust-lang.org/rust-by-example/trait/ops.html

Operator Overloading In Rust, many of the operators can be overloaded via traits. That is, some operators can be used to accomplish different tasks based on their input arguments. This is possible because operators are syntactic sugar for method calls.

operators are syntactic sugar for method calls.

FYI, again, that corresponds to what I have explained in
Inconsistency of Type of the operator #227
>Binary operator/operation is identical to binary function:

So from the first, I'm afraid to say that I have not taken the performance issue seriously.

@shuckster
Copy link

@mAAdhaTTah - Thanks again for the reply here, and for having the discipline I didn't have for putting arguments in their right place. To say one short thing about performance, I do appreciate the work done by engine implementors, but to have them influence the language feels very much like the tail wagging the dog.

I'm sorry that I'm not in agreement with you on these "community at large" arguments, because in terms of both language appearance and also implementation you appear very much on the side of some kind of community or statistical consensus. To bring in another animal-based metaphor, "If I asked people what they wanted, they would have said faster horses". I think Henry Ford would have given us F#. 😁

Finally, on that tiny thing...

Destructuring would add to the cognitive load if you had to rename:
const { value: total } = new Pipe(20) | double | addTwo

Apologies if you weren't referring to me specifically, but this renaming makes it easier for me. I can see the word "total" now, so I can pretty confidently infer that the LHS is the result of the whole pipeline, and can easily parse its RHS construction as I mentioned before.

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Sep 23, 2021

To say one short thing about performance, I do appreciate the work done by engine implementors, but to have them influence the language feels very much like the tail wagging the dog.

They're the ones who have to implement it. If you want a feature implemented, you need their buy-in. Whether we like it or not, that's the state of things right now.

To bring in another animal-based metaphor, "If I asked people what they wanted, they would have said faster horses". I think Henry Ford would have given us F#.

I suggest your metaphor is reversed, and in fact, what you want is a "faster horse": a direct translation into syntax of the pipe / compose functions you currently use and by extension, enabling the curried/point-free styles you're familiar with via that syntax. We're offering the "car" of the much more expansive & powerful Hack pipe, the ability to drop currying & point-free in favor of a functional JavaScript that's closer stylistically to mainstream JavaScript. Just like Ford's car required an investment in new infrastructure like traffic lights & laws, this change will require new libraries & idioms to adopt it across the ecosystem. But (imo, obvs) the end result is a significantly more powerful feature & language.

@shuckster
Copy link

I think of it more that F# already has the roads, and Hack is bringing a horse onto the freeway.

Dang, this is quite the impasse, eh? 😁

@mAAdhaTTah
Copy link
Collaborator

Then the metaphor falls apart; Ford wasn't introducing cars into a world with roads & freeways. Ford was explicitly saying he didn't give the people what they asked for; he gave them what they needed. You're asking for F#; you need Hack. You can have the last word if you'd like, but we should call off this tangent as off-topic.


Back on topic, the usefulness of the Hack pipe if we ended up being able to implement it via operator overloading, I previously said RxJS-style importable methods was a use case that makes sense to me as being necessary to support in the language via syntax, and doing it via operator overloading was a compelling use case. Having chatted with @js-choi about this, he directed me both to his proposal and Hax's proposal for syntax to solve that problem specifically. Like anything, there's no guarantee one or the other (or some combo of both) will definitely advance, but I think it makes more sense to solve that particular problem with direct syntax than trying to shoehorn it in via operator overloading.

Because of this, I think we should treat operator overloading is an orthogonal proposal with its own considerations, rather than something to worry about in relation to |>.

@js-choi js-choi added the follow-on proposal Discussion about a future follow-on proposal label Sep 24, 2021
@dy
Copy link

dy commented Sep 28, 2021

@runarberg some points regarding | operator overloading were raised/discussed in #190.
It was locked with arguments:

  • | has wide usage and pipe semantic may cause breakage
  • | is ambiguous for parsers
  • | has precedence issues
  • operator overloading is still an immature proposal, too long to wait
  • | is incompatible with async code
  • there is zero chance of it getting thru the committee even if any of the champions wished to pursue it

I found that only the last argument was hard to debate.

@runarberg
Copy link
Author

@dy As I understood it #190 was a different issue. #190 was overloading the | operator instead of inventing a new operator |> for pipelining. This issue is about a potential for confusion in case the operator overloading proposal advances along side the hack pipeline operator (|).

This issue is based on the premise (which might not hold) that if the operator overloading proposal advances into the language, many library authors will overload | operator to provide simple pipelines with their constructs. This premise might turn out to be false even if operator overloading advances. Perhaps because new syntax makes it unnecessary (such as the bind operator ->) or simply because it becomes an unpopular choice by library authors for some reason.

Now do the issues raised against overloading in #190 apply when | is overloaded to pipe functions in userland?

| has wide usage and pipe semantic may cause breakage

I’m sure operator overloading won’t advance unless they could be sure it won’t cause breakage. As is you have to opt into the overloaded operators with a statement such as:

with operators from Construct;

So this does not apply here.

| is ambiguous for parsers

Same as previous

| has precedence issues

Now this will be true of operator overloading unless the spec finds a way to mitigate it. For example this will be a parser error:

with operators from Pipe;

new Pipe(1) | (x) => x + 2
new Pipe(1) | (x) => x + 2
            ^^^^

Uncaught SyntaxError: Malformed arrow function parameter list

To fix this you must parenthesize the arrow function on the RHS:

new Pipe(1) | ((x) => x + 2)

This is annoying enough that it might prevent popular usage of the generic Pipe class above who’s only purpose is to overload the | operator.

Operator overloading is still an immature proposal, too long to wait

This applies here but not in #190 and is still relevant. The potential for confusion exists regardless of the time past between advancements of either proposal. E.g. say hack pipes |> advance fast and are included in ES2022 (unlikely but for the sake of argument), while operator overloading won’t be introduced until ES2025. Then in 2026 a popular library overloads the | operator and encourages the usages of it in pipelines. Now you have many users potentially confused because of how similar this feels to the hack pipes |> they are used to until now, but still so different in very subtle ways (including the precedence issue above).

| is incompatible with async code

I don’t think this is true. A library could export AsyncPipe along side Pipe which provides a different overload for the pipe operator:

const AsyncPipeOps = Operators(
  {},
  {
    right: Function,
    "|"({ value }, fn) { return new AsyncPipe(value.then(fn)); },
  },
);

class AsyncPipe extends AsyncPipeOps {
  value;
  constructor(value) {
    super();
    this.value = value;
  }
}

I honestly don’t see the point in this though because it doesn’t really improve much over just using .then directly. And I doubt such library would be popular except maybe maybe as an abstraction over async iterators:

import AsyncIter, { map, filter, reduce, fromBuffer } from "my-async-iter-lib";
with operators from AsyncIter;

const gettingSize = fromBuffer(buffer)
  | filter((chunk) => chunk.startsWith("abc"))
  | map((chunk) => chunk.length)
  | reduce((sum, size) => sum + size, 0);

console.log(await gettingSize);

Note: Annoyingly—because await has a higher precedence the |—we can’t put the await after the assignment operator in this example. It will only await on the results from fromBuffer(buffer) as opposed to the entire pipeline. So we have to return the promise of the entire pipeline and then call await on the result.

There is zero chance of it getting thru the committee even if any of the champions wished to pursue it

This applies only to #190 not here.

@tabatkins
Copy link
Collaborator

I'm going to go ahead and close this issue. The champions are not interested in pursuing a pipe operator spelled |, for several reasons given in this thread and #190. We're also not planning on delaying this proposal until operator overloading is mature (that will be some time, if it ever happens, and most of the reasons we're not interested in a | pipe will still apply).

If anyone wishes to pursue this as a follow-on proposal, that's fine, but it will be in its own repository, not here. ^_^

@runarberg
Copy link
Author

runarberg commented Oct 1, 2021

@tabatkins I think you might be misunderstanding. This issue is not about perusing a pipe operator spelled |, nor about delaying this proposal until the operator overloading is mature. It is about concerns which may arise if creating pipelines using operator overloading in userland becomes a popular choice of library authors. I don’t know why it was tagged with “Follow up proposal” when a “question” was a more appropriate tag.

The concerns being that: a) the hack pipeline operator |> might not be needed, and b) that developers used to the hack pipelines would be confused by the functionally similar (but still subtly distinct) pipelines created with an overloaded operator.

@ljharb
Copy link
Member

ljharb commented Oct 1, 2021

Those are considerations for operator overloading. We shouldn’t be holding back a stage 2 proposal because a stage 1 proposal might obviate the need for it.

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

No branches or pull requests