tc39 / proposal-pipeline-operator

A proposal for adding a useful pipe operator to JavaScript.
http://tc39.github.io/proposal-pipeline-operator/
BSD 3-Clause "New" or "Revised" License
7.51k stars 108 forks source link

Effect of Hack proposal on code readability #225

Closed arendjr closed 2 years ago

arendjr commented 2 years ago

Over the past weekend, I did some soul searching on where I stand between F# and Hack, which lead me into a deep-dive that anyone interested can read here: https://arendjr.nl/2021/09/js-pipe-proposal-battle-of-perspectives.html

During this journey, something unexpected happened: my pragmatism had lead me to believe Hack was the more practical solution (even if it didn't quite feel right), but as I dug into the proposal's examples, I started to dislike the Hack proposal to the point where I think we're better off not having any pipe operator, than to have Hack's.

And I think I managed to articulate why this happened: as the proposal's motivation states, an important benefit of using pipes is that it allows you to omit naming of intermediate results. The F# proposal allows you to do this without sacrificing readability, because piping into named functions is still self-documenting as to what you're doing (this assumes you don't pipe into complex lambdas, which I don't like regardless of which proposal). The Hack proposal's "advantage" however is that it allows arbitrary expressions on the right-hand side of the operator, which has the potential to sacrifice any readability advantages that were to be had. Indeed, I find most of the examples given for this proposal to be less readable than the status quo, not more. Objectively, the proposal adds complexity to the language, but it seems the advantages are subjective and questionable at best.

I'm still sympathetic towards F# because its scope is limited, but Hack's defining "advantage" is more of a liability than an asset to me. And if I have to choose between a language without any pipe operator or one with Hack, I'd rather don't have any.

So my main issue I would like to raise is, is there any objective evidence on the impact to readability of the Hack proposal?

ducaale commented 2 years ago

@lozandier Given the overwhelming support for Smart Lozandier Mix, perhaps it might be better to open a dedicated issue for it?

lozandier commented 2 years ago

@lozandier Given the overwhelming support for Smart Lozandier Mix, perhaps it might be better to open a dedicated issue for it?

I'm not opposed to this at all!

I'll yield to @js-choi, @tabatkins, @mAAdhaTTah, and other current pipeline operators to confirm if that's welcomed as it seems they exclusively create such threads in this repo.

Further deep discussion of the "Smart Lozandier Mix" in this thread can be argued off-topic with the original purpose of this thread discussing the impact of the Hack proposal on code readability (despite the fact that topic prompted its manifestation as an alternate style proposed) technically.

tabatkins commented 2 years ago

@benlesh

Chaining arithmetic operations with |> is a bit rough looking. x |> ^ + 2 |> 10 / ^ Who would do that when they can just do 10 / (x + 2)?

They wouldn't, certainly; that's a small enough bit of arithmetic that it's easiest to read just written out in one expression. Unfolding that into a pipe of one-operation-per-step would be silly, harming readability in the same way that non-trivial arithmetic can quickly become unreadable in langs like Common Lisp that lack infix operators.

But as arithmetic gets more complicated, it can be good for readability to break it apart into distinct steps, where each is a self-contained and meaningful chunk of arithmetic; sometimes it's good to use variables for each of these sub-results, but they don't always have or need semantic names.

Heck, even in the small I think this can improve readability in some cases; trying to do a modulus with JS's remainder operator is kinda annoying currently: ((a % b) + b) % b. Every time I have to type it out, I add close-parens as I realize I need them, then go back and add the correct amount of open parens to the beginning. With a pipe, it's a % b |> ^ + b |> ^ % b - slightly longer, but no longer needing extra parens to be inserted to resolve order of operations properly.


@lozandier

This basic approach (two pipeline operators, one in F#-style and one in Hack-style) has already been suggested by a few people, but the champions have rejected it so far.

The largest issue with it is a general language-design objection: it's rarely a good idea to have two very similar ways of doing something presented at the same level of convenience. Python enshrines this as a guiding principle, for example, as it being good for there to be a single "Pythonic" way to do any given thing, and tho one can't adhere to that principle 100%, it's resulted, for the most part, in a pretty good language imo. (Contrast that with Perl, which makes "there's more than one way to do it" as a proud guiding principle.)

The problem with this is that having two almost identical features makes teaching harder (you need to get into the details immediately just to explain what the two are), and makes usage harder (you have to decide which one is more convenient on a case-by-case basis), and makes reading harder (you have to recognize which one-character-different variant you're reading to understand the code properly; this is especially bad if they're mixed in a single pipeline as it's very easy to assume they're all the same and have your eyes skip over the difference).

Sometimes this can be justified, if there are two equally-important use-cases that are similar at a surface level, but are very difficult or impossible to write in one form or the other. Even then, tho, it's usually better to bless one with a shorter, more official-looking form so it'll be what people reach for by default, and allowing people to teach the second form as a special-case for the rare times it's needed.

This specific form of the idea has some additional issues, in that you propose that RHSs not being functions in the F#-style and not using placeholders in the Hack-style are allowed and meaningful. This means that it's very easy to typo your pipe operator and have code that doesn't error until run-time, possibly at a point far away from the actual typo. A version where the two are completely disjoint in syntax, and optionality is explicit (see #159), would avoid these issues and ensure that accidentally typing the wrong pipe op or similar common typos would be an early syntax error.

I believe @js-choi is interested in pursuing a "second pipe operator" in the near future, either as an F#-style pipe or as a function-composition operator, or something else in that realm that would help HOF-style code more directly. However, we're not currently interested in pursuing it as part of this proposal; as discussed in #221, we've already had issues in the past when we failed to get F#-style pipes to Stage 2, and as far as we can tell, committee interest hasn't significantly budged in that regard. Presenting it alongside the Hack-style pipe likely won't help; the issues the committee had with it are still there, and now there's a nearly identical operator also being proposed that doesn't have the same issues.

See #202 for a discussion of possibly doing a complementary F#-style proposal, tho.

lozandier commented 2 years ago

This basic approach (two pipeline operators, one in F#-style and one in Hack-style) has already been suggested by a few people, but the champions have rejected it so far.

What I'm proposing is meaningfully different which I've explicitly addressed; do you have direct objections to the differences I elaborated about in the section in my proposal directly addressing this?

What I'm proposing has a meaningful difference that I think has substantial uptick in acceptance by several facets of the JS community that have wanted or observed this proposal's progress. I think it warrants more serious consideration than this.

The problem with this is that having two almost identical features makes teaching harder (you need to get into the details immediately just to explain what the two are) and makes usage harder (you have to decide which one is more convenient on a case-by-case basis)

it's usually better to bless one with a shorter, more official-looking form so it'll be what people reach for by default, and allowing people to teach the second form as a special-case for the rare times it's needed.

It's for this very reason why I'm explicitly proposing F#-style is the default as it's typical pipelining behavior people expect while the pipelining semantics involved with Hack-style is far more involved and unorthodox!

As I mentioned before, it's fundamentally problematic that F#-style, a style that models first-class functional composition tacitly–requires more characters than Hack-style, a pipeline style that is far more verbose with its usage of |> and is unorthodox to what pipelining is. The character tax should be something involved with Hack-style.

Asked but not yet answered by a current pipeline champion: Hack-style has no qualms/concern adding additional characters to be able to support generic RHS expression being linearized , so why would it not bear an extra character (>) tax to activate instead of the ubiquitous tacit intent of pipelining to linearize functions represented by the F-style/minimal-style in this proposal repo historically?

I believe if another pipeline operator needs to be considered to allow tacitness in the far future rather than the immediate future, |> is reserved for that usage in the future, not Hack-style. It can't be walked back that a far more verbose way of pipelining has |> reserved to it–the only way around that in the future would be doing something like

"use tacit pipelining";
const result = 2 |> double |> add10 

I'm not sure that's really clamored for by anyone.

As far as teaching, I'm very confident the behavior of pipelining involved with Hack-style immediately involves teaching to be had that isn't needed with F#-style, that follows more conventional ideas of how pipelining behaves that Hack-style deliberately makes more complicated in order to better accommodate any RHS expression instead of functions for an idea that is fundamentally functional.

I think this can be raised and be proved to be the ubiquitous understanding of pipelining across multiple disciplines. It's certainly the case in data science and mathematics.

I see the value of Hack-style making the modeling of problems in a pipeline-oriented matter more approachable to a wider demographic, but not at the expense of hijacking a critical reason why a |> operator typically exists: To communicate functional composition with a left to right sequential flow using the same amount of non-white characters as composing the functions around an initial input.

baz(bar(foo(x)))

is the same characters to write by design but potentially clearer with those with a pipeline mentality with:

 x |> foo |> bar |> baz.

( and ) are replaced by |> and the functional name (or singular result of an implicit lambda function represented by bare values) follows the pipe.

I cannot reiterate enough to any current pipeline champion involved with the proposal in the past, currently, and in the future that this is a critical reason why there's opposition with Hack-style taking |>. I would even accept Hack-style to be the first pipeline operator ratified as long as it's not reserved to |>.

Some people have more extreme takes of accepting no pipeline operator at all rather than Hack-style having its utility reserved to |> because of this. I would be in that camp too if it's insisted that Hack-style is represented with |>.

I find Hack-style being representing as |>–reserving the most tacit way of communicating pipelining–as an overwhelmingly undesired outcome of this proposal that I don't think this proposal will ever easily overcome; I anticipate dedicated focus of this proposal reaching stage 4 will continue to go awry with the champions needing to address why |> is reserved to such a verbose, unorthodox (yet meaningfully novel to allow any RHS expression) means of pipelining.

Having two ways to pipeline is already needed more than any recent proposal to date. The reaction to my proposal–especially compared to the previous smart mix proposal–can't be dismissed as not being a significant signal this proposal needs to consider such an uncommon but precedent result of the proposal resulting in two meaningful new additions to the language via the two operators.

Deciding what to use on a case-by-case has always been a familiar reality to JS developers with the language embracing that as a multi-paradigm-friendly language.

JS developers are constantly divided between

That said, the beauty of the language is that devs/teams can choose and it's very lintable/enforceable an opinion through something like @nzaka's ESLINT. Why won't the pipeline proposal champions allow that to be an option with pipelining with two pipeline operators? It seems extremely arbitrary, and inviting unnecessary discontent of the proposal's progress and continued existence.

I'd wager more likely that attempts will be made to override Hack-style |> with conventional pipelining that F#-style better represents vs. Hack-style being assigned |> to then live on to have well-received feedback being available in the language in things like JS developer surveys.

I'm of the opinion this is a proposal that should have considered leveraging something like Chrome's origin trial program for viable real-world reactions by important JS stakeholders of the language. The @babel plugin for the pipeline operator is downloaded 147,571 times a week after all.

Mixing the two can be convenient as I demonstrated, but overall will be rare. Being two different operators, it'll be easy to make that decision for yourself (or for an entire team/org enforcing this via again linters like ESLint)

it's rarely a good idea to have two very similar ways of doing something presented at the same level of convenience

But it happens; it's not unprecedented. There seems to be a hypothetical fear by current champions of it being a bad idea because of cognitive noise deciding between the two when in reality it can be as easy to navigate as the difference to use single quotes, double quotes, or back ticks in reality.

To tacitly pipeline with functions and non functional values use |> (F-Style), use |>> if you want to more granularly pipeline a non functional expression that is then piped to the next unit of work or complete the pipeline using the result of that expression.

I would personally argue it's much harder to explain public and private fields–or frozen vs. sealed objects–than the effort these operators would take. Even the concept of why bare values are piped through in my examples is that the ability for bare values not needing to be represented as a function with both operators is because they can be presented as functions that return that value; rather than making you have to do that (forcing you to only use functions with either operator), the runtime passes the value to the next value.

I'm extremely optimistic that this is one of those cases it's not only a good idea to have two operators, but one of the only ways for some pipeline champions to get a generic linearization operator for any RHS expression and what an overwhelming majority of the JS community wanted when they had interest of this proposal: A tacit functional composition operator. This includes again programers with new/renewed interest in JS than ever before because this proposal that'll allow a native way to pipeline like they can with their primary languages of choice (i.e. data scientists who use langs like Julia with a tacit pipeline operator).

I believe @js-choi is interested in pursuing a "second pipe operator" in the near future, either as an F#-style pipe or as a function-composition operator, or something else in that realm that would help HOF-style code more directly. However, we're not currently interested in pursuing it as part of this proposal; as discussed in #221, we've already had issues in the past when we failed to get F#-style pipes to Stage 2, and as far as we can tell, committee interest hasn't significantly budged in that regard

Please confirm the belief when you next get the chance. Also, who is exactly "we" with dramatically different champions for this proposal than recent years (creating opportunity for a different outcome)? In any case, the committee should be reengaged with these perspectives–isn't that the responsibility of you and other pipeline operator champions of this proposal to do on behalf of issues raised by the JS community in addition to the opinions shared amongst the champions as a group?

While @js-choi may have engaged the committee about two pipeline operators, I surmise @js-choi did not engage the committee with a smart mix like the variant I've proposed, nor attempted to sell the idea leveraging the important arguments I've outlined that are meaningfully distinct from the original smart mix proposal.

At minimum, I'm not aware of face-to-face transcripts indicating @js-choi proposed what I'm proposing. Accordingly, I don't think this idea has yet to have a fair shake of being considered I do hope you do eventually between my time away from such discussions.

As far as the existing champions, I've been asked to consider being a champion by previous champions of this proposal such as @littledan ; with proposals like this in mind, I'm happy to be coached up towards such a role in 2022 by you and others after running by how it can be relevant to what I do as part with the Google responsible innovation stakeholders I primarily serve. Due to personal matters involving the ongoing pandemic, the health of my biological mother, and the aftermath of the Haitian Earthquake and floods in NOLA had on my significant others, I cannot realistically engage in such activities in 2021.

This specific form of the idea has some additional issues, in that you propose that RHSs not being functions in the F#-style and not using placeholders in the Hack-style are allowed and meaningful

No; thanks for this opportunity to perception check with you nonetheless. My proposal is merely existing F#-style or minimal-style semantics being available via |> and existing Hack-style behavior via |>>.

I merely pointed out that both can further align with conventional pipelining semantics that's potentially beneficial/convenient to the use of either by seeing bare values such as 2 consistently as (() => 2 ). I pointed out if Hack-style takes any valid RHS expression, it's only logical that "foo" |>> 2 returns "2"

When Hack-style is doing things like 2 |>> 2 + ^ to result in 4, from a conventional pipeline standpoint, it's conceptually doing an implicit unary function–((x) => 2 + x)–that so happens not to need (^).

Your modulus example (Hack-style)

a % b |>> ^ + b |>> ^ % b

is merely

a % b |>> (x => x + b)(^) |>> (x => x % b)(^)

This behavior shared by both allows values to pipe to all remaining functions to completion to get a single result from the pipelining, which is consistent with the primary purpose of pipelining: Given an initial value, pass/pipe along the value to one or more functions that uses the output of the previous fruitful func as the input for the following fruitful func till a final result from the final fruitful function is returned. For 2 |>> "foo" or 2 |> "foo" returning "foo" to make sense, the bare value is considered an implicit fruitful function that returns the bare value.

Allowing bare values to be implicitly represented as functions allow both pipeline operator to always be operating with functions and always the same result so users can more efficiently communicate their pipelines without unnecessary verbosity.

With this in mind

Both (Hack-style)

2 |>> "foo" |>> 9 ||> console.log(^) 
2 |>> (()=> "foo")(^) |>>  (() => 9)(^) |>> console.log(^)

And (F-style)

2 |> "foo" |> 9 |> console.log 
2 |> (()=> "foo") |>  (() => 9) |> console.log

Can be argued outputting the same result: undefined is the result of this pipeline and 9 is logged in the console if this concept of pipelining is accepted to make both more powerful, flexible, and more teachable.

This pipeline paradigm convention being adopted with both also argues consistent utility of ^ for both after someone suggested that ^ be banned with minimal style |> that I explained is problematic with very valid, idiomatic uses of ^ with F-style |> they failed to grasp at the time.

While I elaborated on why that behavior can be useful for both pipeline operators be able to more embrace being tighter , already mentioned in the readme, I'm not tied to this being a requirement with my proposal.

My example above hopefully demonstrates

1) It can be argued expected behavior for bare non-functional values be merely passed to the next function or returned by the pipeline since bare values can be modeled as merely implicit functions that return the bare value

2) With 1) in mind, restrictions like F#-style only taking functions (which I'm not totally against) are somewhat odd since bare values that are not functions can again be modeled merely as implicit functions that return the bare value and the RHS expressions that Hack-style accepts are conceptually implicit unary functions.

Accordingly, it can be argued it's merely syntactical sugar to allow bare values pipe to the following function or returned to complete the pipeline. I recognize some have concerns about "hidden bugs" by embracing such pipelining, but that's honestly a common con of this paradigm in general; but the pros outweigh the cons for most developers who model their problems in a pipeline-oriented manner:

For the meaningful benefit of efficient functional composition towards a single result in a terser or simpler manner than imperative programming, you have the cost of mistakes immediately being piped to meaningful tasks further down the pipe you modeled.

This isn't that different than the runtime costs associated with eval, ,, runtime-oriented processing, and so on. I think the concerns associated with things like that are somewhat being overemphasized

lightmare commented 2 years ago

I merely pointed out that both can further align with conventional pipelining semantics that's potentially beneficial/convenient to the use of either by seeing bare values such as 2 consistently as (() => 2 ).

Such semantics is not transferable; non-callables are not the same thing as nullary functions in JavaScript. Not to mention that in languages where values are nullary functions, applying a nullary function to some argument(s) is most likely an error.

What you tout as "conventional pipelining semantics" is not even a thing in my bubble, much less convention. I'm still curious where it comes from.

... if this concept of pipelining is accepted to make both more powerful, flexible, and more teachable.

benlesh commented 2 years ago

What you tout as "conventional pipelining semantics" is not even a thing in my bubble, much less convention. I'm still curious where it comes from.

@lightmare, this doesn't seem to add anything other than tone.

In either case, I'm not a huge fan of non-function values on the RHS of a pipeline. It's one of my gripes with the Hack pipeline. IMO, 1 |> 2 |> 3 should be an error because 2 and 3 are not callable. Similarly, IMO, |>> with no ^ should be an error. Although in that case I'm not sure what you'd call it, because it was never "callable"... but it should be an error.

lozandier commented 2 years ago

I merely pointed out that both can further align with conventional pipelining semantics that's potentially beneficial/convenient to the use of either by seeing bare values such as 2 consistently as (() => 2 ).

Such semantics is not transferable; non-callables are not the same thing as nullary functions in JavaScript. Not to mention that in languages where values are nullary functions, applying a nullary function to some argument(s) is most likely an error.

What you tout as "conventional pipelining semantics" is not even a thing in my bubble, much less convention. I'm still curious where it comes from.

... if this concept of pipelining is accepted to make both more powerful, flexible, and more teachable.

  • it isn't more powerful; 1 |> 2 |> 3 with your interpretation is the same as 1, 2, 3; no power added, just a different symbol for an existing operation
  • it is more flexible; not necessarily a Good Thing
  • it isn't more teachable; you're introducing a foreign concept (equating non-callables to nullary functions) which doesn't apply anywhere else in the language; [1, 2].map(3) won't return [3, 3]

Again, you seem to not realize that pipeline operator is a form of , but tigher, which is again pointed out in this proposal's own readme for the uninitiated inexperienced or unfamiliar with the pipeline paradigms relationship with , that explicitly states, quote:

[Pipelining] is tighter than only the comma operator ,

A pipeline typically does from a conceptual standpoint result in 1, 2, 3 when presented in 1 |> 2|> 3 which you can again interpret as 1 |> (() => 2) |> (()=> 3).

Second, flexibility that is being discussed is to have consistent behavior towards getting a result from a pipeline that is consistent with what pipelining is for: You have an initial value and you have tasks you want to run against the initial value towards a specific value of a result of it; the functions ran in the pipeline may have side-effects but are ultimately fruitful if they return a value. JS functions at least return undefined allowing you to chain tasks towards the completion of a pipeline pretty easily that is further enabled if non-functional values bare values are automatically piped to the next function in the pipeline–or returned if they're the last value of a pipe.

You don't seem to realize expressions are implicitly unary functions to return things to a REPL as ()=> <return value of expresion>; that's the basis of main in C-style languages such as Go.

Accordingly, a bare non-functional value can be interpreted as a unary function that returns the value to pipe that value to the next function for pipelining aligning with its functional , nature; if no more functions exist to run against the bare value, the pipeline is done as a conceptually valid concept.

Finally your third example, [1,2].map(3) is an extreme reach in your streak of whataboutisms: Array.prototype.map explicitly expects a function and is not in anyway a , or pipelining context; Array.prototype.map is not expecting any result to consume as an end result that pipelining and , is doing–it is merely wanting a function (first-class object with[[Call]]) to invoke as the first item in its arguments object.

All that said, I'm again not strongly opposed to non-functions values not the result of running a function being an error with the use of |> (F#-style); if that is the case then, expressions without ^ should be banned from Hack-style (|>>) to be consistent, as @benlesh pointed out (Hack-style fundamentally allows non-functional RHS to be piped to an end result). Enforcing those safeguards are congruent of wanting to make pipelining in JS with |> and |>> a tighter version of , than it conceptually can be.

I'm totally FINE with that; This aspect of the pipeline paradigm is not something I feel is necessary to enable in JS at this time.

It would merely just be syntactical sugar to make sure your pipeline declaration with either operator is purely functional that may perhaps be too convenient to enable at this time.

Perhaps it's something that's allowed in some way in the future. It's not at all critical or relevant to my proposal outside of pointing out how it can be conceptually valid with a pipeline-oriented mindset of solving problems. I've provided you several examples; my apologizes if I failed to enable you to emphasize with how some pipeline-oriented problem solvers would interpret the code examples you and others like @noppa raised.

The only objection I found questionable of yours is the example you characterized as confusing of an unexpanded "2" |> parseInt10 |> add(^) pipeline expression–add(parseInt10(^)) which is the equivalent of add(parseInt10(2))(2)–fundamental to how pipelining works regardless of either operator.

mAAdhaTTah commented 2 years ago

All that said, I'm again not strongly opposed to non-functions values not the result of running a function being an error with the use of |> (F#-style); if that is the case then, expressions without ^ should be banned from Hack-style (|>>) to be consistent, as @benlesh pointed out. Those things are congruent of wanting to make pipelining in JS with |> and |>> a tighter version of , than it conceptually can be.

pipe as implemented in most functional libraries don't have these value insertion semantics, so I don't think these semantics are going to be intuitive for JavaScript developers because that's not how their current tools behave. I suspect x |> 2 and the like are more likely to be developer mistakes (along the lines of the x |> bar.{foo,goo} example above) than they are intentional value insertions. If that's desired in the current world, you'd use a function like R.always to communicate that explicitly, and I think the |> should require that explicitness as well.

Relatedly, I think having a placeholder in both versions is confusing, and it's not clear to me that x |> foo(^, 2) desugaring to foo(x, 2)(x) is particularly useful. This is also a case where my expectation as a developer is that, seeing a placeholder, the expression will be evaluated "as-is", rather than evaluated with the placeholder then applying the returned function to the same placeholder value (and, given the above value insertion semantics, if the evaluated expression isn't a function, are we just back at Hack-style semantics?).

Lastly, I think the visual similarity between |> & |>> exacerbates the above issue because of how easy it would be to accidentally type >> instead of > and opt into different placeholder semantics than you expect. Admittedly, the visual similarity of the operators is less of an issue if they differ significantly in behavior (e.g. placeholder only in Hack-style), but as currently proposed, I think it would be very easy for someone to write x |> foo(^, 2) |> ...rest of pipeline... and wonder where & why undefined was getting inserted into the pipeline (assuming foo doesn't return a function).

Each of these things are fixable; however, if you change all of these (drop value insertion, placeholder only in Hack style, swap |>> for |:), you're back at the original Split Mix we previously explored. The example in the wiki uses your symbols but I was somewhat preferential to this version, although which symbol to use is less relevant than the other issues.

Those are at least my specific objections. Because the original Split Mix doesn't have these footguns, I think it's better than this version of the Split Mix, and I think both of those versions are worse than just doing F#, as per the reasons that Tab explained above.

lightmare commented 2 years ago

Again, you seem to not realize that pipeline operator is a form of , but tigher, which is again pointed out in this proposal's own readme for the uninitiated inexperienced or unfamiliar with the pipeline paradigms relationship with , that explicitly states, quote:

[Pipelining] is tighter than only the comma operator ,

They're talking about operator precedence: that pipe |> binds more tightly than comma , Has nothing to do with their behaviour.

If they wanted pipeline operator with the semantics of comma, they could've used this idea of automatic topic variable binding in comma expressions, and then Hack-pipe wouldn't need a new operator because comma could do what Hack-pipe does. But this idea has downsides that a dedicated operator with clearer semantics does not have.

A pipeline typically does from a conceptual standpoint result in 1, 2, 3 when presented in 1 |> 2|> 3 which you can again interpret as 1 |> (() => 2) |> (()=> 3).

In which language does this interpretation work?

ducaale commented 2 years ago

Although it might effectively kill the partial application proposal, I still think the split mix propsal is an interesting one.

@mAAdhaTTah Is it possible to update the docs for split mix so it doesn't use two seemingly similar operators i.e |> and |>>?

mikesherov commented 2 years ago

I could be completely missing the point, again, but if we agree on a "2 different operators" proposal, and we agree on |> earning the F# semantics, why even debate how mixing or Hack style would work right now, unless there's a specific BC concern we need to address up front?

lean into the organizational advantage and focus created by that agreement, if indeed the agreement is there: defer the hack proposal and land the F# proposal which has potentially less syntactic and semantic cases to think through.

the other advantage of the F# proposal landing is it creates a completed prereq for either hack and/or partial application to land later. This group gets more feedback about using pipelines in the wild, and multiple solutions to any ergonomic issues discovered exist!

SRachamim commented 2 years ago

I agree. Let's land the |> pipeline operator first, and then the ? PFA and/or |>> Hack (whatever it's named) operator. Start small, familiar, minimal.

aadamsx commented 2 years ago

This dual operation line of discussion is not going to go anywhere with TC39 people. Just read the thread until the end https://github.com/tc39/proposal-pipeline-operator/issues/234. If smell even a hint of confusion it will get crushed.

tabatkins commented 2 years ago

What I'm proposing is meaningfully different which I've explicitly addressed; do you have direct objections to the differences I elaborated about in the section in my proposal directly addressing this?

My general objections to "two extremely similar operators with extremely similar spelling" applies regardless of the specific details; all variants of "split mix" suffer equally from this objection.

For the specific details that vary from the previous "split mix" (just two operators, one F#-style, one Hack-style), taking from this comment, I believe they are:

  1. The F#-style operator uses the placeholder to get partial-application (PFA); val |> foo(^, 2) evaluates the RHS per partial-application rules to result in a function akin to x=>foo(x, 2), then calls it with val.
  2. If the F#-style operator gets a non-callable on the RHS, it just evaluates to that value (rather than trying to call it and throwing an error).
  3. The Hack-style operator allows anything on the RHS, including no placeholder-usage at all; you just get whatever the RHS evaluates to.

Unfortunately, I think these changes make the proposal significantly worse than the prior "split mix", for several reasons.

  1. Allowing the two operators to significantly overlap in valid syntax is an enormous footgun. Sometimes they give the same result, but often they won't, erroring at runtime instead (possibly in the pipeline, possibly not until some time later when the corrupted data is finally used). For example, observable |> map(x=>{...}) evaluates to a mapped observable (assuming map() is from RxJS or similar library), while observable |>> map(x=>{...}) evaluates to the mapping function itself, throwing the observable away. This is extremely easy to typo, and extremely hard to spot in the middle of code. (@lightrose argued this in more detail.)
  2. Allowing the placeholder to do PFA in the F#-style gives it a completely different meaning to the placeholder in Hack-style; they're similar only in simple examples. For example, in val |> foo(bar(^, 1), 2) is the RHS a function equivalent to x=>foo(bar(x, 1), 2)? Or is it a the result of evaluating foo(x=>bar(x, 1), 2)? The latter is what PFA requires (and @rbuckton's design for PFA has good reasons for that), but the former is what the Hack-style operator would be equivalent to.
  3. Similarly, Hack-style allows the placeholder to participate in arbitrary expressions; PFA doesn't, for good reasons (it wants to be able to eagerly evaluate every aspect of the function call, args and all, at definition time). That is, foo(? + 1) is disallowed in PFA currently; val |>> foo(^ + 1) (Hack-style) would be fine, but would val |> foo(^ + 1) be allowed or disallowed? Again there's a conflict between "consistent placeholder behavior" and "PFA design".
  4. Also, this PFA-ish design takes the air out of PFA elsewhere - var x = arr.map(obj.foo(?, 1)) likely wouldn't become possible, especially if the pipe version of it has different semantics. That seems pretty unfortunate; that's the main place I want to use it!

We're already confident that a plain "split mix" wouldn't make it thru the committee (or else we'd have proposed it; it would have avoided a ton of the anger seen in the last few weeks), and I think this version would be substantially less likely to succeed.

tabatkins commented 2 years ago

This duel operation line of discussion is not going to go anywhere with TC39 people. Just read the thread until the end #234. If they're against even the hint of confusion it will get crushed.

Note that #234 was about "Smart Mix" (both pipeline semantics in one operator), which is distinct from "Split Mix" (two operators, each with a different pipeline semantic).

mikesherov commented 2 years ago

We're already confident that a plain "split mix" wouldn't make it thru the committee (or else we'd have proposed it; it would have avoided a ton of the anger seen in the last few weeks), and I think this version would be substantially less likely to succeed.

wasn't sure if you were responded to me or not, Tab, but what I was saying wasn't to adopt either split mix, smart mix, or hack at all. Just saying that a potentially reasonable starting place is to adopt the F# proposal for |> and defer to the future the specific way in which either PFA, HOF, and/or Hack levels up from there.

you can solve the "syntactically similar" argument with deciding on a less similar syntax. You can decide to do/not do split mix, smart mix, PFA, etc.

pipelines have been talked about for years, and at some point, the "smallest possible step that ladders up" becomes more attractive, no? Am I missing a reason this doesn't ladder up?

tabatkins commented 2 years ago

It wasn't directly addressed to you, no, but I'm happy to address it here. (These threads get way too long too quickly; if I tried to directly address everyone I'd be spending my whole day.)

The "start from F#-style with |>, do something else later" has been proposed before. We've rejected it for a few reasons:

  1. As discussed in #221 and the history document, we've already failed to get F#-style pipes to Stage 2 in the committee, and it doesn't seem likely that the prior objections have changed. So that kills the idea immediately.
  2. It locks us into either only doing F#-style, or later doing a "smart mix" or "split mix" situation. Both of the latter have also already been rejected by the committee to a greater or lesser degree.

pipelines have been talked about for years, and at some point, the "smallest possible step that ladders up" becomes more attractive, no? Am I missing a reason this doesn't ladder up?

The issue is just that there is no smallest possible step. All of the syntaxes (there are at least 4 extant syntaxes across languages) are closely equivalent in power; none of them are a "simplest version" of the idea or syntactically compatible with each other.

We've tried some variants already; they didn't pass the committee. We've done our best to document the history of our approaches over the past several years; it might still be lacking in some respects, but I think it's fairly thoro at this point. Hack-style is the first version that's made it thru.

mikesherov commented 2 years ago

Thanks for the history lesson 👍

SRachamim commented 2 years ago

Did we fail to push the minimal proposal? That's just function application.

tabatkins commented 2 years ago

Yes, "minimal" F#-style (just function application) got strong pushback from several people on the committee (including myself) because it wasn't compatible with awaiting async values. To handle the equivalent of val |> await foo(^) |> bar(^) in the current proposal, you'd have to do one of the following:

// I *think* you don't need more parens around the await here;
// precedence is confusing.
let result = await (val |> foo) |> bar;

let result = await (val |> foo).then(bar);

let result = await (val |> foo |> async x=>bar(await x));

all of which seemed real awkward.

The more developed proposal added a bare await special case, so you could write:

let result = val |> foo |> await |> bar;

The "bare await" special-case definitely was not the cause of F#-style failing to advance; it was the opposite.

SRachamim commented 2 years ago

But await should not be used in a pipeline at all. For this you have then. That's not a good reason to decline a pipeline operator that should just allow to pipe a value through one or more functions.

That's funny. This async/await was introduced to avoid a pipe-style code! You're using await when you want to name its result. It's absurd. We'll absolutely miss the point if we'll try to allow magic other than the ever-simple a |> f === f(a).

Idiomatic code:

const f = a => a |> g |> fetch

const i = a => a |> h |> j

return fetch('api.com')
  .then(f)
  .then(i)

Pipe should be used with nothing but functions. It's a simple concept. Let's try to keep it simple.

I see no real reason to decline a |> f === f(a). It's just a syntactic sugar for an already known concept of a function application.

tabatkins commented 2 years ago

Yes, you can pipe thru .then(), especially if you start with an async function. But the async function might come in the middle of your pipeline, at which point you have to choose one of the methods I listed above, all of which are awkward. The point of await is precisely to let you keep your code looking like "normal JS" when dealing with async values, and that applies just as well in the middle of a pipe as anywhere else.

Regardless, tho, the point is that await is an important part of JS, and its existence is figured into the design of all new features; we've had significant discussions about accommodating it in the match(){...} statement proposal, for instance. We can't ignore it here. And if we'd tried to ignore it anyway, the pipeline proposal would have been blocked. You can look up old issues in this repo on the matter for further details.

SRachamim commented 2 years ago

What is a good reason to use await and not then? It's way harder to handle errors with await. So why? The only one reason is: it looks imperative. You get to name your result. You can put it in a const. It's absurd to do flip flops in order to force it into a pipe.

Those who desires the |> already prefers then over await.

The point is: you already have an imperative way (await) and a declarative way (then) to handle promises. |> is a declarative feature. For some reason, you try to force the imperative way (await) into a declarative feature (|>).

SRachamim commented 2 years ago

What you really want, are different small proposals. If you'll aim for small proposals you'll solve more issues. For example, instead of having a special treatment for await, why not having special treatment for an interface? Say we have those small proposals:

// Mapping a promise alias proposal
Promise.prototype.map === Promise.prototype.then

// Reverse function application proposal
a & f === f(a)

// Reverse map application proposal
a <&> f === a.map(f)

const f = a => a & g & fetch

const i = a => a & h & j

return fetch('api.com')
  <&> f
  <&> i

Then this <&> will work for any value that implements a map, not only a promise. It will work also for an Array, or even for a custom { map: a => b }.

Now, I'm ok with having a syntactic sugar especially to .then, as long as the minimal pipeline operator stays simple (no special treatment to await). I just wanted to make a point that proposal for new features should stay minimal, without trying to "catch them all".

tabatkins commented 2 years ago

I'm not willing to argue about whether or not people want to or should use await. Having new JS syntax play nicely with await whenever possible is a strict requirement.

SRachamim commented 2 years ago

I'm not willing to argue about whether or not people want to or should use await. Having new JS syntax play nicely with await whenever possible is a strict requirement.

Says who? This await turns out to be serious blocker to a very simple feature we want to add. We should discuss about whether this is justified.

This attitude won't lead the JavaScript community anywhere. Let's close this discussion, since you don't even want to justify those "strict requirements".

Anyway I encourage you to take a closer look at the "whenever possible" part. Maybe this is the case when it's not possible, since we're talking about a pipeline operator that should pipe a value through one or more functions, and await is not a function?

Nazzanuk commented 2 years ago

At the risk of being repetitive, I think it's worth stating that the Hack proposal gracefully handles pretty much every scenario discussed here in a fully consistent manner. Absolutely no special rules are needed for await or yield, no difference in treating unary functions vs non-unary, and an explicit syntax for every operation with the single "downside" being an extra 3 characters for unary functions.

There is no need for split or smart mixes adding confusion, or to even wait for the PFA proposal to pass stage 1. This focus on purity just for unary functions is extreme.

Whilst language decisions should be informed and influenced by the implementations of prior languages, I think this fixation on F# style unary functions is focussed too much on minimal syntax and not enough on utility. It does a disservice to the ease of use and harmonious application Hack style will offer for n-ary functions, arithmetic, array literals, object literals, template literals, promises etc.

aadamsx commented 2 years ago

Note that #234 was about "Smart Mix" (both pipeline semantics in one operator), which is distinct from "Split Mix" (two operators, each with a different pipeline semantic). @tabatkins

I was just pointing out that if TC39 won't consider a placeholder drop only on unary functions (what you called Smart Mix), for example:

value |> foo(^) // VALID: unary function calls can be written this way value |> foo // ALSO VALID: or for unary function calls (and only unary function calls) they can also be written without ^ placeholder, in these cases the compiler will assume placeholder position

// No Mode switching here, think of it as a option short-hand for unary use cases in Hack style only world. value |> foo |> ^.bar() |> fooBar(^) |> yetAnotherFooBar(1, ^) // VALID syntax, where foo & fooBar are unary functions

Then why would they let anything more substantial, like real function composition along-side Hack to get through?

lozandier commented 2 years ago

At the risk of being repetitive, I think it's worth stating that the Hack proposal gracefully handles pretty much every scenario discussed here in a fully consistent manner. Absolutely no special rules are needed for await or yield, no difference in treating unary functions vs non-unary, and an explicit syntax for every operation with the single "downside" being an extra 3 characters for unary functions.

There is no need for split or smart mixes adding confusion, or to even wait for the PFA proposal to pass stage 1. This focus on purity just for unary functions is extreme.

Whilst language decisions should be informed and influenced by the implementations of prior languages, I think this fixation on F# style unary functions is focussed too much on minimal syntax and not enough on utility. It does a disservice to the ease of use and harmonious application Hack style will offer for n-ary functions, arithmetic, array literals, object literals, template literals, promises etc.

@Nazzanuk This following point has been extensively communicated to you and pipeline champions to no avail, but I'll try again once more in addition to reiterating no explicit answer has been given why |> must be associated with Hack-style pipelining that has been problematic to many for some time since the style was proposed. I think that needs to be more explicitly addressed by the existing pipelining champions other than @mAAdhaTTah who did today by proposing |:.

An overwhelming amount of people in these discussions don’t mind the aim or utility of Hack-style–I actually welcome it for the benefits you just outlined that's very accessible to beginners of solving problems in a pipeline-oriented way; its usage is like training wheels for those that don't normally functionally compose by doing () => <expression> potentially and (x) => <expression with x being available via^>; for those familiar with the ubiquitous purpose of pipelining to tacitly communicate first-class functional composition, it's a casual means of expressing what you would express within a body of a unary function pipeline fundamentally leverages for its casual computational representation that's very teachable

Many don't even mind that much Hack-style being the first pipeline operator introduced…

Just don’t take |> for the use of Hack-style that makes |> essentially permanently unavailable to other styles that more align with the ubiquitous understanding that |> is for tacitly communicating first-class functional composition for their utility in the future.

It cannot be reiterated enough to people like you in this discussion that are in the minority wanting Hack-style to be tied to |>. What |> ubiqioustly symbolizes is in direct contrast to the style of pipelining associated with Hack-style: |> is one of the most well known tokens to communicate tacit first-class functional composition with a left to right sequential flow using the same amount of non-white characters as composing the functions around an initial input that communicates data processing right to left.

Once more,

baz(bar(foo(x)))

requires the same amount of non-space characters to write by design as an alternate approach of communicating the functional composition that may be potentially clearer than the code above. This clarity can resonate so much to people towards them further investing in solving problems with a pipeline-oriented approach towards wanting it in the language one day:

 x |> foo |> bar |> baz

( and ) are replaced by |> and the functional name (or singular result of an implicit lambda function represented by bare values if allowed) follows the pipe.

Hack-style–with its deliberate/meaningful verbosity for the sake of RHS expression flexibility–being associated with |> so commonly known for tacitly communicating functional composition is severely contrary to what an overwhelming amount of people wanted to problem solving contexts well beyond programming using |>. I'm of the strong opinion that this requirement for Hack-style to be represented as |> is arbitrarily and unnecessarily being associated with |>. It's a distraction for Hack-style's sensible reason to be natively supported in the language some day.

Why the heck should tacit forms of pipelining aligning with the primary purpose of pipelining require more characters to activate their utility instead of Hack-style that does away with prioritizing tacitness (combative of the purpose associated with an operator) to more generically accommodate any expression–including expressions that are combative of elementary synchronous pipelining like await and yield? Fortunately that's fixable (technically such other styles can adopt other one or two character tokens.)

That said, it cannot be easily walked back Hack-style being tied to |>. It should daresay be considered harmful to all current and prospective JS developers wanting to model pipeline problems in JS. In its current form, it can easily exist on its own without being tied to |>; it's too deviant from the ubiquitous utility of |> to represent tacit first-class functional composition–concise representation of unary functions without manually invoking them–to be tied to that token. To be clear, this is not to say Hack-style is not tacit as a whole (otherwise it would have zero claims to |>, in my opinion); it's tacitly communicating the body of unary functions that requires you to use (^) to compose results with functions within such implicit unary functions..

**Hack-style in short is a tacit way of linearizing functions, but it's not a means of modeling unary models tacitly that conventional pipelining is ubiquitously understood as being in languages/markup where functions are first-class citizens. Accordingly, hack-style is an odd fit for |>, which historically has been tied to this proposal being associated with tacitness/terness of representing functional composition that Hack deliberately rejects:

Hack-style disincentivizes communicating a functional composition left to right instead of right to left like normal–even when it may more sense to from a conceptual standpoint.

Hack-styles makes the most basic kind of function composition–with unary functions–harder to write than calling them normally:

  x |> foo(^) |> bar(^) |> baz(^)

is far more characters to write than the code below despite the the code below being arguably harder to understand than the code above:

   baz(bar(foo(x)))

Concerned about this reality with hack-style, many vehemently want to hold on to the idea of tacit functional composition pipelining behavior being tied to |>–regardless if Hack-style becomes ratified with an indefinite hold on a tacit means of communicating first-class composition in JS in the meantime.**

Again, the most essential representation of a pipeline task is a unary function; unary functions being more verbose to write than expressions that are far rarer to pipeline with via the hack-style pipeline operator being tied to the most ubiquitous form of |> people want in JS permanently unavailable to more tacit forms of pipelining in the future is something a great deal of subscribers of this proposal repo object to.

Hack-style being tied to |> is a severe, arbitrary roadblock associated with Hack-style right now that logically doesn't hold up well. I'm bewildered why this isn't more understood with why I think @js-choi's previous/current attempts to have a F#-style be revived to better accommodate the JS community have been panned. I'm bewildered why this seemingly has been opaque to current champions of this proposal.

Pipeline is too ubiquitous with tacitly communicating first-class functional composition with tokens like |> for Hack-style to also associate its unorthodox piping to when it doesn't have to. I'd love Hack-style to be in the language, but not at the cost of F#-style and other tacit means of communication functional composition forever taxed with additional characters to have their utility available to developers instead of Hack-style that isn't tacit and deliberately embraces this allowing you represent unary functions purely as RHS expressions (that's what it's ultimately doing).

It's an extreme stance of the current pipeline champions insisting Hack-style is represented by |>. Accordingly many have begun taking an extreme stance of their own, deciding they would rather have no pipeline operator passed at all to indefinitely use pipe() and other prominent approximations used by millions of developers everyday (including those who maintain such helpful approximations such as @benlesh) if it meant Hack-style wouldn't be associated with |>.

Please "read the room" better on this stance that has been expressed vehemently the past 3+ years on the manner, and a critical benefit of communicating the pipeline operator through code in a magnitude of disciplines. To bring levity to this discussion, there is no alternate universe that it's logically sound that |> meant for tacit communication of functional composition has a pipeline operator not intended for that purpose at all be assigned to it.

Of all the pipeline styles proposed, Hack-style is overwhelmingly least deserving of the use of |> for the reasons it's ubiquitous understood for, nor can it justify it compared to the styles that JS developers have had experience with approximations and informal trials via Babel/TypeScript forks the past few years to continually want pipelining in the language.

I think @mAAdhaTTah has provided a very pragmatic solution of Hack-style being represented as|: or something else; as I stated when I suggest my original proposal, I'd like Hack-style to have its utility utilized by two characters just like F#/minimal-style/explicit-piplining via |>.

This proposal is merely in stage 2; as pointed out by @tabatkins himself, it is more than reasonable for myself and many throughout the JS community requesting this simple change towards Hack-style's native availability in the language I welcome. This is a very reasonable solution to fix this issue without necessarily replacing Hack-style with F#-style or minimal-style, two styles that unfortunately haven't gotten the traction to reach Stage 2 yet as Hack-style has I'm continually bewildered by (especially the latter that's easy to build on top of incrementally as @mikesherov and many others over half a decade have pointed out to no avail). I'm still optimistic a tacit means of representing first-class functional composition will happen before I have gray hair and/or retire; I hope I don't have to wait till I'm 84 years for this to happen though:

I think these discussions are becoming helpful of a more appropriate token for Hack-style having its utility available to proponents of it such as yourself without being problematic to an increasingly obvious majority opinion that it shouldn't be tied to common tokens associated with tacit first-class composition–which it explicitly doesn't do.

Prior to @mAAdhaTTah's being open to Hack-style being represented as something other than |> via :|, I didn't think current champions could say in good faith they are engaging with the JS community sufficiently enough for the Hack-style to be both ratified and welcome by a significant amount of the JS community with open arms in addition to active work of leaving room for tacit first-class composition pipeline operators to later exist and have intuitive tokens to be home to for their utility in the language.

While I'm adamant a tacit, first-class functional-composition pipeline operator should ship in the language as originally desired by this proposal, I'm even more adamant hack-style doesn't prematurely reserve the ubiquitous means of representing such operators for its usage.

For those that resonate with this, please consider this as my "Associating Hack-style with |>does more harm than good to the language" post. 🙃 ,

aadamsx commented 2 years ago

An overwhelming amount of people in these discussions don’t mind the aim or utility of Hack-style–I actually welcome it for the benefits you just outlined that's very accessible to beginners of solving problems in a pipeline-oriented way; its usage is like training wheels for those that don't normally functionally compose by doing () => potentially and (x) => <expression with x being available via ^>; for those familiar with the ubiquitous purpose of pipelining to tacitly communicate first-class functional composition, it's a casual means of expressing what you would express within a body of a unary function pipeline fundamentally leverages for its casual computational representation that's very teachable

Many don't even mind that much Hack-style being the first pipeline operator introduced….

Just don’t take |> for the use of Hack-style that makes |> essentially permanently unavailable to other styles that more align with the ubiquitous understanding that |> is for tacitly communicating first-class functional composition for their utility in the future.

I toally agree with this. I think any reasonable person can see Hack will be great addition to the language, and I'm pleased TC39 is considering it in its current form. Just please think twice before taking the the traditional functional composite operator |> for Hack.

aadamsx commented 2 years ago

For those that resonate with this, please consider this as my "Hack-style being associated with |> is considered harmful" post. 🙃 ,

Why don't you start a new issue for this? Seems appropriate topic of discussion to me.

ducaale commented 2 years ago

Also, where does @mAAdhaTTah mention another character?

@aadamsx I believe it was https://github.com/tc39/proposal-pipeline-operator/issues/225#issuecomment-929249973

lozandier commented 2 years ago

Relatedly, I think having a placeholder in both versions is confusing, and it's not clear to me that x |> foo(^, 2) desugaring to foo(x, 2)(x) is particularly useful. This is also a case where my expectation as a developer is that, seeing a placeholder, the expression will be evaluated "as-is", rather than evaluated with the placeholder then applying the returned function to the same placeholder value (and, given the above value insertion semantics, if the evaluated expression isn't a function, are we just back at Hack-style semantics?).

Not having a placeholder for variants of pipelining related to F#-style and minimal-style represented as |> is a no-go to some as it would effectively unnecessarily discriminate against very valid HOC functions pipeline flows

I'll demonstrate this by using a curry-friendly version3 of const add = (x, y) => x+y after parsing a potential valid number with a unary function that parses its as input and invokes parseInt(potentialNumber: unknown radix: Number) with radix already being set to 10 ; this is a more real-word and more correct version of what was raised ago without casual typos ("2" was being casually sudden thrown around as 2 & etc) days ago:

An everyday person may feel compelled to write the following

parseInt10("2") |> add(^) 

With conventional pipelining in mind this is

add(parseInt10("2"))(parseInt10("2"))

It being conventional in first-class composition to represent foo(bar(x)) as x |> bar |> foo, it follows that parseInt10(2) |> add(^) is further unnested to be represented entirely as a "flat" pipeine1.

"2" |> parseInt10 |> add(^) 

It's pragmatic and conventional to completely unnest what I began with and what @noppa originally raised a few days ago; again–in more simpler terms–the primary purpose of a pipeline operator is to avoid saving intermediate results without having to embed function calls within one another (first-class composition from left to right in traditional western languages) in order for it to fit its purpose as a functional composition operator2.

This is ubiquitous across professions beyond programming as well towards justifying my recommendation that tokens associated with tacit first-class composition pipelining/linearizing are avoided by Hack-style to activate its utility; I trust experienced, prominent language designers such as yourself, @tabatkins, and other TC39 members can figure it out.

Accordingly, I have to respectfully disagree banning placeholder topics with the use of F#-style/minimal/explicit pipelining |> being obviously advantageous. Nonetheless, it's not unprecedented to limit |>to just unary functions; Julia–an important programming language among data science professionals–does this. This would be sufficient for existing library authors such as @benlesh who have millions of dev stakeholders of their libraries that adopts tacit functional composition pipelining semantics with their library code / approximations of its behavior such as the way you can model problems in a pipeline-oriented manner with Observable / Subject.

However, unlike JS, Julia has macros that allows you to override operators. We don't have that in JS yet. I would be fine with limiting |> (not Hack-style) to unary functions only on the basis operation overloading will be in the language (ideally a prerequisite seemingly to ensure it can allow placeholder topics used in the matter I think I eloquently explained).

Because the TC39 proposal to do operator overloading exists, many ideally wanted minimal proposal to go through for now for basic functional composition (similar to the sentiments of recent comments in this thread).

Finally, it would be completely unintuitive to ban placeholder topics with |> further if TC39 decides to go ahead with Hack-style (|>>/|:) having the ability to express RHS expressions without (^) that opens the door for it being pushed for the sake of consistency that |> can evaluate bare non-functional values as implicit unary functions that returns those values [(()=> <value>)] that placeholder topics become important for it to have for devs be able to consistently represent non-functional values without topics with either operator.

This is ubiquitous to people experiencing solving problems in a pipeline-oriented manner functionally or mathematically to the extent such people could reasonably contend a placeholder topic shouldn't be banned.

Footnote

1: It's conceptually understood with a tacit pipeline operator and modeling problems in a pipeline manner everything to the left before an earlier placeholder topic is inserted to the next placeholder topic.

2: It is for this reason that bare values are often desired to be spared an unnecessary functional call of wrapping them as (()=> <value>) instead of value. That is why some (not me vehemently) people experience with pipelining from all walks of like can try and argue bare values should just be passed over to the next subroutine as its starting input to work with or the final result of a conceptional pipeline. This is common in data processing context when you're primarily modeling problems as a pipeline to always get a result. I hope this illuminates where that perspective often comes from.

3: In the real-world, many who like to model problems in a pipeline manner or other functional-oriented ways who still very much want regular invocation still occur use nAry semantics to have const add = (x, y) => x+y callable as add(2, 2) or add(2)(2) simultaneously with the same function. add is a silly example as it's far more desirable for that kind of function to take any number amount of parameters to add and use nAry abstractions to make it callable as add(2, 2) or add(2)(2) or other specific amount of arguments.

ducaale commented 2 years ago

Not having a placeholder for variants of pipelining related to F#-style and minimal-style represented as |> is a no-go as it would effectively unnecessarily discriminate against very valid HOC functions pipeline flows

@lozandier I think the more universal partial application operator would be a better fit for the F#-style pipeline operator

import { map, partition } from 'lodash'

const x = [1,2,3]
  |> map(?, x => x * 10)
  |> partition(?, x => x % 3 === 0)
lozandier commented 2 years ago

@ducaale Hmm, I clarified "to some" a few minutes before your comment regarding it being worthwhile or notto initially ban placeholder topics or not. That said, I'm aware of the pipeline applications operator; it's been similarly stuck in standards abyss for a while similar to this proposal until recently. However, I wonder sometimes if it's a side-effect of this very proposal being slow to advance for interest/urgency be renewed for it to be actually ratified.

I mentioned operation overloading so that the pipeline operator can use the hypothetical partial application operator to pipeline with it. 😃 .

ljharb commented 2 years ago

It's not a side effect; pipeline could hit stage 4 tomorrow or be permanently killed, and PFA would have the same difficulties in front of it, as would operator overloading. Pipeline (in any form) may certainly be used in concert with those two proposals, but that doesn't mean their future is tied to pipeline in any way.

lozandier commented 2 years ago

@ljharb Agreed; I was more stating I wonder speculatively if the champion(s) of that proposal value more clarity of how placeholder topics are perceived by others–or how others typically think they behave like–prior to unifying on a solution they're comfortable with proposing towards stage 4 acceptance in a timeline they're happy with.

voliva commented 2 years ago

I'm sorry I'm a bit late to the party, and also the fact that this issue is getting too much activity, but I'd like to mention another thought on why Hack looks versatile but it might not be as useful as F#

Hack essentially removes the need for intermediate temporary variables that will be used just once on the next line - Which raises the question, why don't people just use one temporary variable and keep reasigning it? Grabbing @mAAdhaTTah 's example https://github.com/tc39/proposal-pipeline-operator/issues/225#issuecomment-923349047 it would become:

let $ = userId;
$= await fetch(`${GET_DATA_URL}?user_id=${$}`);
$= await $.json();
$= {...$.data, ...input };
$= await fetch(`${POST_DATA_URL}`, {
  method: "POST",
  body: JSON.stringify({ userId, data: $ })
});
$= await $.json();
$= $.data;
return $;

In here I used $ and formatted in a way which is also similar to the hack's pipeline (by replacing |> for $=), but my question is not about the fact that it's similar, but that this pattern above is usually a sign of a code smell. If I were to see this in a codebase, I'd much rather have the original code where every intermediate value is given a self-documenting name rather than having to cognetively track what $ means on every single step.

F# pipelines on the other hand will be sintactic sugar for a pattern that's being used on different cases (point-free, rxjs, etc.), which is to use a pipe(...args) function. That pattern it's being used in many places because it helps with readability: Every single step within the pipeline has a descriptive name of what it's going to happen.

I'm also on the side that if Hack pipe end up taking over F#'s, I'll have a hard time finding cases where I can use it and it will make my code actually better. Given that case, I'd also push for the suggestion of having both operators in place with distinctive tokens.

ljharb commented 2 years ago

@voliva that question has been answered in this repo many times - reassigning variables is a bad practice and causes hard to understand code. See #207 and #173. Adding a self-documenting name for every step is often onerous and can add more noise than signal.

arendjr commented 2 years ago

Adding a self-documenting name for every step is often onerous and can add more noise than signal.

This is a nice way of putting it, because it makes it very clear that your “solution” is to just throw out the signal all together, which is my main readability objection against Hack in the first place.

ljharb commented 2 years ago

@arendjr i absolutely agree that this is the primary value of F# pipes. However, the problem with $= is not the repetition (which occurs in Hack also) but the reassignment, which causes issues with closures. Hack does not suffer from this footgun, because ^ is captured by closures correctly/intuitively.

lozandier commented 2 years ago

Adding a self-documenting name for every step is often onerous and can add more noise than signal.

This is a nice way of putting it, because it makes it very clear that your “solution” is to just throw out the signal all together, which is my main readability objective against Hack in the first place.

Exactly. It's sufficiently clear enough, by design, that |> is explicitly communicating in a tacit manner that the value on the left will be piped to the following function (orwhat what can be inferred as functions such as bare values can be (i.e. what @mAAdhaTTah identifies as the functionality of R.always() –just like how ( and ) tacitly communicates the function to the left of it is composing the function within it.

This allows reversing the representation of functions |> and () to be harmoniously analogous to each other in the effort it takes to write them. Hack-style jeopardizes this by owning |>, which is extremely problematic for a great deal of people since Hack-style surfaced as an option for this proposal. |> is well known of being associated with pipelining behavior Hack-style doesn't embraces by design.

Hack-styles's use of (^)for functions is unnecessarily verbose in the mental model of what most think of when they think of ways to tacitly communicate functional composition. |> is too commonly associated with that mental model for Hack-style to be associated with it for its utility. |: is perhaps a good start.

arendjr commented 2 years ago

the problem with $= is not the repetition (which occurs in Hack also) but the reassignment, which causes issues with closures. Hack does not suffer from this footgun, because ^ is captured by closures correctly/intuitively.

Sure, but you're still ignoring our actual concerns. @voliva was not arguing that we should just adopt reassignments instead, he explicitly mentioned code looking like that is sign of code smell. He says, in bold:

I'd much rather have the original code where every intermediate value is given a self-documenting name

And this was my original concern as well, which I still don't see adequately addressed despite the almost 200 comments in this thread. And sure, Hack might fix this footgun over the strawman we don't want: it doesn't address the readability concerns over the code we don't want.

Instead, the best we got is some people saying they actually do prefer code looking like that, at which point it becomes a game of he said/she said.

I tried adressing this too, when I mentioned it might be valuable to find out whose values are more aligned with the community at large. But unfortunately, this too, is yet unaddressed.

voliva commented 2 years ago

@ljharb

reassigning variables is a bad practice and causes hard to understand code.

That's exactly the point I want to bring across.

Hack pipelines are not that far from reassigning variables, the only advantage pipe has is that it avoids the closure issues that reassigning variables would possibly have, so that might partially solve the "bad practice" of it. But it doesn't solve the "causes hard to understand code" part of it.

You will still have to read, parse and think what each step in the pipeline does to understand what is that code doing, compared to reading explicit variable names or function calls. The fact that it could be buggy in some special cases when you use internal closures is only one of the disadvantages of reassignment.

That's the whole point of this discussion I think. Do we really need an operator that will help writing code that will be hard to read and understand? Specially when we have an alternative which would promote function composition.

SRachamim commented 2 years ago

@Nazzanuk I'm not sure it's wise to design a feature in a way that can be used in any way. That's not how I see programming language design. A good programming language is one that won't allow incorrect use of its features.

While you see JavaScript as a language that anyone (including untrained developers) should be able to write - I want to see it grow and evolve to a point in which is a serious tool for developing professional large-scale production code - and it can be done without sacrificing popularity.

If you'll allow developers to use a number when a string is expected - that's a bug, not a feature. And when you design a pipeline operator in such a way that it allows non-function values to the right - that's a bug, not a feature.

While all programming languages are making a progress, it seems like sometimes JavaScript takes steps backwards.

If you want to attract data-scientists and FPers to the language - you can't let yourself ruin a pure FP feature like |> just for the sake of letting others to use it in a non-FP way. That's A-level absurd. It's like FP programmers will ask to change a class feature to have an FP style. We should design OOP features in OOP style, and FP features in FP style.

This way of thinking (let's design everything so it can be used in any way) - will create a situation where all the features are compromised. That's a very weak point.

Isn't the list of language quirks too big already? You want that list bigger? It's funny that in order to asses a JavaScript developer, you need to test if he can cite all the possible language quirks and anti patterns. To be a good JavaScript developer you need to know how not to use it.

And the Hack proposal will make it even worse.

Minimal proposal:

Symbols to learn: One (|>). Concepts to learn: Zero (Function application is already a known concept). Possible number of quirks introduced: Zero.

Hack proposal:

Symbols to learn: Two (|> and ^). Concepts to learn: |> is an entire new concept. Possible number of quirks introduced: Infinite.

While the minimal proposal is just a syntactic sugar (you can actually summarise it as a |> f === f(a), barely need a spec!), the Hack proposal is an entire new invented feature (you need a spec, and you need to mitigate many possible confusions and quirks, and conflicts with other current features).

How can one miss the simplicity? A pipeline operator must not work with any feature. It should work only with functions, because this is what it is.

If you want to make it easier to work with non-function inside a lambda (as in a |>, a .then or a .map) - then tackle it with other proposals that will sort those limitations out in all contexts, not only in |>.

Proposals should do one thing. They should do it well. They should do it only.

arendjr commented 2 years ago

@SRachamim's argument reminded me of the Pit of Success.

To quote:

a well-designed system makes it easy to do the right things and annoying (but not impossible) to do the wrong things

In my opinion, the Hack proposal, by making it easier to write code without naming any temporaries encourages people to write code that is less readable and less maintainable. It might be great from a writability perspective, but from a readability and maintainability perspective, we're sending them into the Pit of Despair instead.

mAAdhaTTah commented 2 years ago

There's a lot here that I can't respond to in depth, but I just want to clarify that I was not suggesting I was open to Hack-style being |:. To re-up how I closed that post:

Those are at least my specific objections. Because the original Split Mix doesn't have these footguns, I think it's better than this version of the Split Mix, and I think both of those versions are worse than just doing F#, as per the reasons that Tab explained above.

[Emphasis added]

In sum, what I meant to imply, and could have said clearer, is as follows:

  1. The Split Mix proposal in this thread is more foot-gun-y than the Split Mix we considered 3 years ago.
  2. In that original Split Mix, we proposed |:, which I preferred over |>>, and my memory was that's what we used in the Wiki (but that was incorrect).
  3. Both Split Mix proposals are more complicated than going with F#.
  4. I still prefer Hack to F#, so my personal rankings are Hack > F# > Original Split Mix > Lozandier Split Mix.

I'm also not a delegate/champion but a highly-involved community member who's been working on this proposal for the past 3-4 years. As such, I've been involved in the arguments & discussions throughout that time so can help provide some historical context on what's been considered, hence the reference to |:, but I am not a member of the committee so am not involved in advancing the proposal directly.

aadamsx commented 2 years ago

In my opinion, the Hack proposal, by making it easier to write code without naming any temporaries encourages people to write code that is less readable and less maintainable. @arendjr

I see your point, as the placeholder temp variable is not named in Hack. So lets compare the Hack vs F# vs Existing styles, using unary functions:

// F#
value 
  |> extractBusinessData 
  |> mergeWithWebServiceData 
  |> await 
  |> processResults 
  |> printResult;

Above we have nice named unary functions that are descriptive

// Hack
value 
  |> extractBusinessData(^) 
  |> await mergeWithWebServiceData(^) 
  |> processResults(^) 
  |> printResult(^);
// Hack with alternative pipe operator and placeholder
value 
  |: extractBusinessData(:) 
  |: await mergeWithWebServiceData(:) 
  |: processResults(:) 
  |: printResult(:);

But above we still have a good idea what's going on without naming the temp variables right? (by the way, the alternative pipe operator |: looks like it belongs doesn't it?)

// Temp variables
let $ = userId;
$ = extractBusinessData($); 
$ = await mergeWithWebServiceData($);
$ = processResults($);
printResult($);
// Named temp variables
const userId = userId;
const businessData = extractBusinessData(userId);  
const data = await mergeWithWebServiceData(businessData);
const result = processResults(data);
printResult(result);

There's a lot here that I can't respond to in depth @mAAdhaTTah

That's too bad as there is a lot of great comments & questions here from @SRachamim @lozandier @arendjr @voliva and more that should be addressed -- unless it's a done deal as is and not really a point discussing it further.

I've come around to Hack style based on everything I've read, I think any reasonable person would be. It will be a great addition to the language as is I think, but there are still valid questions/concerns that should be addressed by you and the other champions -- and I'm not talking about one-offs. Concerns with using the |> operator when it's been traditionally used for function compsition and concerns about readability with unnamed temp variables -- to name a couple.

mAAdhaTTah commented 2 years ago

@aadamsx Less that I don't want to respond and more that I don't have time right now. I have started writing a longer response to a few things on and off but haven't had time to wrap it and the conversation keeps going in the meantime. Will comment when I have some time to digest things more.

ljharb commented 2 years ago

Personally, I think always having a new, distinctly named variable for each step is often less readable than the pipeline alternative. Due to the concrete hazards of reassignment, i always of course use either nesting, or different variables, in my own code - but I'd much prefer to use (either form of) pipeline there, because i want to omit the variable names, and i obviously don't want a pile of closure-related bugs.

Note I said "often" - it's also almost as likely that using such names would improve readability, in which case i simply wouldn't choose to use a pipe there. Just because you can use a syntax to make code less readable at times, doesn't mean you shouldn't be able to use it to make code more readable (looking at you, ternary and arrow functions).