-
Notifications
You must be signed in to change notification settings - Fork 110
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
Comments
What would be the advantage of using operator overloading rather than just using a |
The most obvious for me is that the pipe operator 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 As for having to have to write 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)); |
Good point.
It's reasonable to observe that the majority of the JS community has longed for more strictness of the language. Especially for Static Typing, 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:
So there is confusion especially, if we have the hack-style first, then F#-style later.
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.
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.
Also, we will need Monad operators, actually, the operator for function application 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.
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. |
I'm sympathetic to the RxJS example because it's narrowly scoped to be used with 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:
I think it would be significantly advantageous to have this in the language proper compared to sideloading it via operator overloading. |
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.
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 |
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.
I don’t think the main problem is that 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
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. |
@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. |
https://github.com/tc39/proposal-operator-overloading/blob/master/README.md#usage-documentation
Bitwise operators seems to have potential for overload. |
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. |
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?
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:
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 Anyway, to lurch this horrible wall of text back on topic, I would say that operator overloading is completely anathema to whatever version of Or in other words, I completely agree with:
(But yeah, let's make it the F# one please. Limitation begets creativity. 😁 ) |
When we talk about operator overload in language level, it should be the comparison between
where |
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. |
I feel terribly sorry for the matter. Really..
There are many people here who share his thoughts.
If it would be a choice of poison or nothing, We want to chose nothing and wait for other features such as operator overloading. |
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:
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.
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:
Destructuring would add to the cognitive load if you had to rename: const { value: total } = new Pipe(20) | double | addTwo |
Performance issue again?
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.
FYI, again, that corresponds to what I have explained in So from the first, I'm afraid to say that I have not taken the performance issue seriously. |
@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...
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. |
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.
I suggest your metaphor is reversed, and in fact, what you want is a "faster horse": a direct translation into syntax of the |
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? 😁 |
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 |
@runarberg some points regarding
I found that only the last argument was hard to debate. |
@dy As I understood it #190 was a different issue. #190 was overloading the 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 Now do the issues raised against overloading in #190 apply when
|
I'm going to go ahead and close this issue. The champions are not interested in pursuing a pipe operator spelled If anyone wishes to pursue this as a follow-on proposal, that's fine, but it will be in its own repository, not here. ^_^ |
@tabatkins I think you might be misunderstanding. This issue is not about perusing a pipe operator spelled The concerns being that: a) the hack pipeline operator |
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. |
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)):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:Then goes on to see something like this:
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:
|>
pipeline over overloaded operators provided from libraries?The following questions might apply more in the operator overloading proposal:
| console.log
vs.| console.log.bind(console)
; potentially avoided with the partial application proposal).|>
and their user defined pipe operator|
?The text was updated successfully, but these errors were encountered: