proposal-pipeline-operator icon indicating copy to clipboard operation
proposal-pipeline-operator copied to clipboard

Impact of Hack pipes on the JS functional-programming ecosystem

Open js-choi opened this issue 3 years ago • 72 comments

Spinning this out of https://github.com/tc39/proposal-pipeline-operator/issues/202#issuecomment-919374937.

Lots of people have expressed concerns that Hack pipes would silo the community of JavaScript tacit-programming (aka point-free style). These fears are understandable. And we are sorry for all the frustration that people who like tacit programming have had—feeling like something was promised but taken away. (See #215 and https://github.com/tc39/proposal-pipeline-operator/issues/206#issuecomment-918609518.)

However, we are hopeful that the opposite of siloing will happen: we are hopeful that Hack pipes would actually help increase interoperability between functional APIs and non-functional APIs. We’re hopeful that this increased interoperability would decrease siloing, rather than increasing it.

(By “functional APIs” I do not mean only tacit programming but libraries that identify as “functional APIs”. Also, as a reminder, tacit programming / point-free style is not the same as functional programming. Tacit programming is only a subset of functional programming. In fact, much functional programming is pointful (e.g., with Haskell do notation). The debate over tacit programming versus pointful style has been a long and controversial one in the FP community for many decades.)

The pipe champion group thinks that keeping userland tacit rx.pipe-style functions within internal code is okay. In fact, that’s what F#’s documentation itself suggests:

F# supports partial application, and thus, various ways to program in a point-free style. This can be beneficial for code reuse within a module or the implementation of something, but it is not something to expose publicly. […]

With little exception, the use of partial application in public APIs can be confusing for consumers. Usually, let-bound values in F# code are values, not function values. Mixing together values and function values can result in saving a few lines of code in exchange for quite a bit of cognitive overhead, especially if combined with operators such as >> to compose functions. […]

In contrast to the previous point, partial application is a wonderful tool for reducing boilerplate inside of an application or the deeper internals of an API. It can be helpful for unit testing the implementation of more complicated APIs, where boilerplate is often a pain to deal with. […] Don’t apply this technique universally to your entire codebase, but it is a good way to reduce boilerplate for complicated internals and unit testing those internals.

In other words, it is true that Hack pipes would keep tacit programming in userland functions and not in the language’s syntax, and that is disappointing to many people who like tacit programming. But we are hopeful that Hack pipes would increase interoperability and decrease siloing between functional libraries and non-functional libraries (and, as F# suggests, tacit programming should be kept internal anyway). I know that not everyone in the community shares this view, and we won’t be pushing for advancement for a while anyway, but hopefully this logic can assuage some fears.

(To emphasize, it is likely than an attempt to switch from Hack pipes to F# pipes will result in TC39 never agreeing to any pipes at all; syntax for partial function application (PFA) is similarly facing an uphill battle in TC39 (see HISTORY.md). I personally think this is unfortunate, and I am willing to fight again for F# pipes and PFA syntax, later—see https://github.com/tc39/proposal-pipeline-operator/issues/202#issuecomment-919374937. But there are quite a few representatives (including browser-engine implementers; see HISTORY.md about this again) outside of the Pipe Champion Group who are against improving tacit programming (and PFA syntax) in general, regardless of Hack pipes.)

In any case, the explainer does not talk about the impact that Hack pipes may have. This is a deficiency of the explainer. We need to fix this sometime.

This issue tracks the fixing of this deficiency in the explainer (lack of documentation regarding projected impact of Hack pipes on the functional-programming ecosystem and decreasing of siloing). Please try to keep the issue on topic (e.g., comments about the importance of tacit programming would be off topic), and please try to follow the code of conduct (and report violations of others’ conduct that violates it to [email protected]). Please also try to read CONTRIBUTING.md and How to Give Helpful Feedback. Thank you!

js-choi avatar Sep 18 '21 00:09 js-choi

@js-choi

Thanks for trying to emphasize with the people on the F# side even when you are on the hack side. As always which such polarizing issues, people almost never have bad intent, but get very frustrated and upset when they think their arguments are ignored, or misunderstood. But the same people react the same to arguments from the other side due to human nature called confirmation bias.

That said, as a disclaimer, even when I try to stay above my own confirmation bias, and try to weigh arguments on their own merits, I'm still biased toward F#. So read with that in mind 😊

But we are hopeful that Hack pipes would increase interoperability and decrease siloing between functional libraries and non-functional libraries

I share your hope. But I strongly doubt it. Especially I think that people used to the traditional style and libraries wouldn't even consider using a, in their eyes, new and fancy pipe construct when what they were already doing was already good enough. Otherwise they probably would have done so already using current possibilities like userland pipe constructs. And when they currently use method chaining, I'm sure they would feel quite at home with F#, where they simply switch from . to |>, whenever such libs emerge.

and, as F# suggests, tacit programming should be kept internal anyway

You use this argument more often, but I still think it's a misleading one. Either F# makes an exception for |> or doesn't consider this as tacit encouraging programming, because |> is used and encouraged extensively there. I think it's the latter. (I know I also keep making the same argument:) technically you could consider F# pipes point-free encouraging because you create a function without naming the final 'point'. But semantically the final point is mentioned directly before the |> instead of after. It is true however that the declaration of the function you are using on RHS must be so that the final parameter can be curried. And here lies the difference in F# and JavaScript, in F# usually all parameters are curried where in JavaScript it is easy to do but, you could argue, unnatural. Still, I think, the 'tacit encouraging' argument is misleading. I'd say it is currying encouraging.


Another thing is: Of all arguments I keep reading in favor of hack, I am missing an in my eyes quite compelling one that I only read in the HISTORY.md file: A concern that it might encourage users to create even more performance degrading closures.

And although I think they really have a point there, I also think that (although I'm not a compiler expert) this can be optimized later. E.g. when such a function is only used in |>, and the curried 'intermediate' function does nothing but returning a function requiring the last argument, I'd say that creating a closure can be optimized away by doing as if the function is not curried, since |> immediately calls the just created closure anyway.

Jopie64 avatar Sep 18 '21 08:09 Jopie64

@js-choi First of all, thank you for opening the issue of this aspect and I also appreciate your hard work (must be, I guess) for Brief history of the JavaScript pipe operator that I think the information listed is extremely valuable for everyone who concerts the matter.

For the "Impact of Hack pipes on functional programming", I would like to paraphrase "Impact of Hack pipes on algebraic structure in JavaScript".

Reading through your work describing the history, I feel very sorry for this proposal, especially @rbuckton's contribution.

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

Replacements an expression of OOP style to a binary operator in an algebraic sense make the code concise and readable, easier to grasp the math structure which leads us to avoid mistakes then the code becomes robust. This is a very powerful approach, and perhaps some people observe this manner as FP code. Haskell codes are full of binary operators, and Haskellers basically do just Math/algebra in their code.

A replacement of a syntax f(x) == x |> f g(f(x)) == x |> f |> g is the same league of ** binary operator.

Replacement and replaceable or equivalent itself is a very powerful concept. Why? Because equivalency reduces complexity. The history of software development is a war against complexity.

If we are very careful to treat programming entities kept equal all the time, we safely can avoid the problem of combinatorial explosion. https://en.wikipedia.org/wiki/Combinatorial_explosion

In mathematics, a combinatorial explosion is the rapid growth of the complexity of a problem due to how the combinatorics of the problem is affected by the input, constraints, and bounds of the problem.

So, to keep entities of programming equal or to be replaceable equally all the time is an extremely important concept, and although I don't like to use this term, many call this advantage referential transparency. https://en.wikipedia.org/wiki/Referential_transparency

Referential transparency and referential opacity are properties of parts of computer programs. An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program's behavior. This requires that the expression be pure, that is to say, the expression value must be the same for the same inputs and its evaluation must have no side effects. An expression that is not referentially transparent is called referentially opaque.

Obtaining a concise binary operator contributes to reduce the complexity of our codes. Math.pow(Math.pow(2, 3), 5) == 2 ** 3 ** 5 g(f(x)) == x |> f |> g

Of course, the above code is referentially transparent. They are Just math equations, and since things are always equal and replaceable, we are guaranteed to be safe from the complexity of combinatorial explosion.

On the other hand in hack "operator", I mean mathematically operator is "pure" in concept but this one is not "pure", we are not safe any longer.

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

The hack "operator" spoils equivalency and referential transparency or introduces a side effect. The side effect is to introduce a new context variable ^.

This fact has been apprehended with certainty here recently: Question about ^ when a "child pipeline" is present. #208 https://github.com/tc39/proposal-pipeline-operator/issues/208#issuecomment-918539647 @mAAdhaTTah

It seems like the presence of a |> in a child function would give new meaning to the ^ on its RHS until the end of the function's context?

This is correct: the |> operator introduces a new expression scope, so the (^) on line 6 refers to what you expect it to.

The reason programmers avoid side effects is this is the factor of complicity and fundamentally this is mathematically inconsistent. https://en.wikipedia.org/wiki/Referential_transparency

In mathematics all function applications are referentially transparent, by the definition of what constitutes a mathematical function. However, this is not always the case in programming, where the terms procedure and method are used to avoid misleading connotations. In functional programming, only referentially transparent functions are considered. Some programming languages provide means to guarantee referential transparency. Some functional programming languages enforce referential transparency for all functions.

Losing referential transparency, a real-world programmer who has used hack-style-operator for a certain period of time actually has been suffered.

https://github.com/tc39/proposal-pipeline-operator/issues/202#issuecomment-922070966 @kawazoe

As a dev who used reactive extensions a lot in my career (whether it's rxjs or rx.net), I've noticed the same problem arise in nearly all the teams that try to use it. They all end up leaking state from the pipeline. In Rx pipelines, this leads to race-conditions and horrible issues with code readability on top of making the code completely impossible to reuse. In the end, there is no way to prevent this, and the pipeline operator style will not influence how people uses Rx that much. The problem isn't with Rx though. It is within limitations of the pipeline mechanism that it uses. As it is heavily based on lambdas, most of the code in Rx captures the scope outside the pipeline and thus depends entirely on the developer to use that captured scope correctly. Any language feature which would discourage this behavior would have saved me a very large amount of wasted time debugging those issues.

Do we really want to choose a pipeline operator that also has this problem by default? With hack style, you can easily extract the state of the pipeline at any point in an external variable. Aka, this code seems valid: value |> (value = ^) |> log(^). By requiring expressions for every step of the pipeline, you end up with a clear path to extract the pipeline state. You might say "who in their right mind would write code like this?" and yet this is the kind of stuff I have seen done in Rx multiple times.

JavaScript is originally developed by Brendan Eich, a functional programmer who had been Scheme enthusiast. That's why JavaScript has a core feature of functional language such as functions are first-class objects.

Now, we have a pipeline "operator" that should have been considered as a very basic binary operator of function application that is a very basic algebra operation in the mathematical sense but actually polluted by a side-effect.

Impact of Hack pipes on algebraic structure in JavaScript

With Hack pipes, we've lost JavaScript as a functional language. Perhaps that's why many are upset. It's not a matter of "interoperability", and "silent majorities" will care about this matter a lot.

ken-okabe avatar Sep 18 '21 19:09 ken-okabe

The side effect is to introduce a new context variable ^`.

Introducing a context variable is not a "side effect." A side effect is a modification outside the local environment. Hack pipe introduces the value into the local environment the same way a parameter to a function does, but it does not modify anything outside of it. The Hack pipe is thus side-effect free.

What's more, it doesn't break referential transparency either. If you took x |> f(^) and replaced it with f(x) (or the value returned by f(x)), the result is the same, which is what referential transparency is; namely, the ability to replace a function with the result of that function without changing the behavior of the application. This is closely related to it being side-effect free. Hack pipe doesn't lose referential transparency.

Hack doesn't break math, nor does it lose JavaScript as a functional language.

mAAdhaTTah avatar Sep 18 '21 19:09 mAAdhaTTah

Introducing a context variable is not a "side effect."

I disagree  

A side effect is a modification outside the local environment.

Only half correct.

With hack style, you can easily extract the state of the pipeline at any point in an external variable. Aka, this code seems valid: value |> (value = ^) |> log(^). By requiring expressions for every step of the pipeline, you end up with a clear path to extract the pipeline state. You might say "who in their right mind would write code like this?" and yet this is the kind of stuff I have seen done in Rx multiple times.

Extracting the context, state from outside is also "side-effect".

Providing a connection or opening an entry-hole to the outside is also a modification. From the outside, we can see the open hole and freely enter.

The definition of the term is not an essence here.

What does matter is we can extract internal variables with the operation of hack-pipe, and that never happens in math function that is referential transparent in the definition.

ken-okabe avatar Sep 18 '21 20:09 ken-okabe

What does matter is we can extract internal variables with the operation of hack-pipe

If you're referring specifically to this quoted line:

value |> (value = ^) |> log(^)

This is equally possible in F#: value |> x => (value = x) |> log. There isn't anything specific about Hack that makes it possible to "extract internal variables" that F# prevents.

mAAdhaTTah avatar Sep 18 '21 20:09 mAAdhaTTah

Extracting the context, state from outside is also "side-effect" and the definition of the term is not an essence here. What does matter is we can extract internal variables with the operation of hack-pipe [...]

To be clear, this is still possible with F# style: value |> x => (value = x) |> log

The difference is that in hack style, this solution is visible by default while in F# style, it requires a lambda. My initial argument was not that it is possible with Hack style, as I believe this problem is impossible to fix in the current language. It was rather that F# style will discourage this behavior by making it more obscure, and that this is caused by inciting currying over multiple argument functions and lambdas.

In other words, it's impossible to prevent side effects here because lambdas in javascript cannot be prevented from capturing their scopes, and thus influencing it. On the other hand, F# style appears to me as an opportunity to discourage this behavior.

EDIT: I see we're on the same line @mAAdhaTTah :)

kawazoe avatar Sep 18 '21 20:09 kawazoe

It was rather that F# style will discourage this behavior by making it more obscure, and that this is caused by inciting currying over multiple argument functions and lambdas.

I don't see why, having seen people do this with lambdas in RxJS, your expectation is that they wouldn't do this with lambdas in F#. I don't think the properties of either proposal makes this more or less likely.

To expand: at the point which a developer decides they need to make a given variable available outside the context, they're going to drop into the syntax that allows them to do that, for better or worse. In Hack, that's value = ^; in F#, that's x => (value = x). That's a mistake a developer using either proposal will make.

mAAdhaTTah avatar Sep 18 '21 20:09 mAAdhaTTah

@kawazoe

Thanks, I should have quote more:

https://github.com/tc39/proposal-pipeline-operator/issues/202#issuecomment-922070966

F# style doesn't prevent this, it at least discourage it by taxing it heavily when compared to calling an FP friendly function: value |> x => (value = x) |> log. This style doesn't expect an expression with access to the pipeline' state by default. You have to declare it.

The variable is declared independently, so this is not a leak. In any way, I've been coding F# style pipeline operator |> and lambda for a long time, and I know it's just a replacement of f(x) and referential transparent operator.

ken-okabe avatar Sep 18 '21 20:09 ken-okabe

My opinion on this is based on an idea that I've seen floating around in other thread about the "tax" that comes with each style. People will often take the easier path and by taxing every call equally with some variant of (^), hack style doesn't incite toward extracting the state or not. On the other had, based on our previous discussions, F# style should incite or at least might incite more people to use curried functions. These functions aren't taxed as much as multi arguments functions in F# because you do not need the x => part at all.

Basically, over time, you would expect to see a lot more code like this: value |> a(1) |> b(2) |> c(3) than like this: value |> x => a(1, x) |> x => b(2, x) |> x => c(3, x). This makes lambdas, and thus opportunity to extract the state, a lot more visible in F# style. At least, assuming a world where currying takes on. If it doesn't, then this whole thing is not going to matter.

kawazoe avatar Sep 18 '21 20:09 kawazoe

@kawazoe Which style do you feel secure in to write a robust code with fewer mistakes?

ken-okabe avatar Sep 18 '21 20:09 ken-okabe

@stken2050 That is a very hard question to answer.

What I bring is my experience working with multiple junior devs on a team and the kind of mistake they often do when dealing with FP style code like RxJs.

I cannot tell if Hack or F# style would make it better or worse. I have seen stuff in this proposal that could very well make things worse. Examples like this one: https://github.com/tc39/proposal-pipeline-operator/issues/167#issuecomment-829636277 which to me is a good representation of the kind of code people will write when they don't understand the reason why the pipeline operator exists. Thing is... I don't think it would be any different with either proposals. This is not a Hack style problem, this is an understanding problem.

I feel like the F# style, purely based on the assumption that it seems to encourage currying more than the other, is the better choice, but I can't say for sure. Not without trying it in a large scale project with other people. I feel like F# style would be easier to explain to people.

kawazoe avatar Sep 18 '21 21:09 kawazoe

@kawazoe

Thank you for your input. May I have another question: It seems the code is valid in hack:

1 |> 5
  |> console.log(#) //5

|> is supposed to be a binary operation of f(x), correct?

5 is not a function, and I don't know what's going on, or I've never seen this stuff in Math etc. Can you explain to me because I'm an inexperienced coder of the hack operator? Thank you.

ken-okabe avatar Sep 18 '21 21:09 ken-okabe

@stken2050 this is off topic, but my understanding is that Hack style does not behave like a binary operator. Rather, it is a syntactic sugar that gets from pipelined to implicit code behind the scene. See the "Real world example, continued" for an idea of what it is doing here: https://github.com/tc39/proposal-pipeline-operator#temporary-variables-are-often-tedious. It explains why you can just put a 5 in the middle and still have valid code.

kawazoe avatar Sep 18 '21 21:09 kawazoe

1 |> 5 is invalid because the topic token isn't used on the RHS, so this would be a syntax error.

mAAdhaTTah avatar Sep 18 '21 21:09 mAAdhaTTah

@kawazoe Thanks. In fact, I'm sure this is on-topic: Impact of Hack pipes on functional programming. and I think it's not adequate to produce too many sub-issues.

@mAAdhaTTah

What's more, it doesn't break referential transparency either. If you took x |> f(^) and replaced it with f(x) (or the value returned by f(x)), the result is the same, which is what referential transparency is; namely, the ability to replace a function with the result of that function without changing the behavior of the application.

 1 |> 5
  |> console.log(#) //5

This seems a valid code of hack, 5 is not a function. Can you replace to f(x) syntax from this code? To me, the hack pipe obviously breaks referential transparency, or am I wrong. Please explain.

If you're referring specifically to this quoted line: value |> (value = ^) |> log(^)

(value = ^) is some value, so I thought I could replace it 5 which is also a value in the principle of referential transparency.

ken-okabe avatar Sep 18 '21 21:09 ken-okabe

Please note: in the current JavaScript, I could replace any (x = y) to z (referentially transparent), because the operator = means the left and right equal from now on and returns the value. Does hack pipe Implicitly overrides the =??

ken-okabe avatar Sep 18 '21 22:09 ken-okabe

@stken2050 Well... based on what @mAAdhaTTah just said, maybe 1 |> (^ = ^) |> log(^) would be valid? Or I guess it cannot be assigned either? That code definitely looks very happy 🤣

More seriously, you could always do 1 |> true ? 5 : ^ |> log(^) to "bypass" the condition. It's not a requirement that the value gets used, just that the marker is present.

kawazoe avatar Sep 18 '21 22:09 kawazoe

This seems a valid code of hack, 5 is not a function.

No, it isn't. It's a SyntaxError.

maybe 1 |> (^ = ^) |> log(^) would be valid?

Cute, but no, you can't assign to ^. It's immutable for the scope of the RHS it's injected into.

mAAdhaTTah avatar Sep 18 '21 22:09 mAAdhaTTah

I mean do they override the JS operator functionality of =: (x = y) replaceable to z with hack?

ken-okabe avatar Sep 18 '21 22:09 ken-okabe

  1. If hack operator does NOT override = operation functionality, value |> (value = ^) to value |> (5) should be valid.

  2. If value |> (5) is syntax error, which means they override = (x = y) has been safely replaceable to any z until now in any places in JS code, hack-operator broke the referential transparency.

ken-okabe avatar Sep 18 '21 22:09 ken-okabe

@kawazoe

that Hack style does not behave like a binary operator.

For the very basic math operation of function application f(x), the famous pipeline operator |> does not behave as the binary operator in JS. This is scary.

Hack doesn't break math, nor does it lose JavaScript as a functional language.

Impact of Hack pipes on functional programming should be unacceptable to silent majorities.

ken-okabe avatar Sep 18 '21 22:09 ken-okabe

@stken2050 No, they don't override =. The pipeline operator only requires that the placeholder ^ is present somewhere in the expression. Its absence is the SyntaxError, not the use of 5. This is why value |> 5 || ^ |> log(^) is valid, even though functionally equivalent to your snippet. In a way, An other way to see it is as if (x, y) => x was a SyntaxError because y is not used.

I agree that, this requirement is a bit strange considering how easy it is to bypass. I think the proposal could have done without it from a user's point of view. There might be some obscure complexities introduced by the compiler that makes this care very hard to handle. Either way, it is still something "weird" to do in a pipeline.

kawazoe avatar Sep 18 '21 22:09 kawazoe

  1. Writing to a variable outside the scope of the lambda (F#) or expression (Hack) is neither referentially transparent nor side effect free in either proposal.
  2. Both pipe operator proposals are binary operators. Both are applications of their LHS to their RHS; the semantics of what and how they're applying is what is different.
  3. I really have no idea what any this has to do with overriding the =, which neither proposal does.
  4. We are way off topic, because none of this prevents you from using Hack as a functional pipe. This argument isn't convincing or compelling to me so I'm going to decline to engage with it further.

mAAdhaTTah avatar Sep 18 '21 22:09 mAAdhaTTah

Thanks for trying to emphasize with the people on the F# side even when you are on the hack side. As always which such polarizing issues, people almost never have bad intent, but get very frustrated and upset when they think their arguments are ignored, or misunderstood. But the same people react the same to arguments from the other side due to human nature called confirmation bias.

That said, as a disclaimer, even when I try to stay above my own confirmation bias, and try to weigh arguments on their own merits, I'm still biased toward F#. So read with that in mind 😊

@Jopie64: Thanks! Of course, you are correct here. Everyone, myself included, comes with our own subconscious cognitive biases, and we all have to watch out for them. And it is understandable for people to be frustrated when they feel like they’ve lost something promised.

But we are hopeful that Hack pipes would increase interoperability and decrease siloing between functional libraries and non-functional libraries

I share your hope. But I strongly doubt it. Especially I think that people used to the traditional style and libraries wouldn't even consider using a, in their eyes, new and fancy pipe construct when what they were already doing was already good enough. Otherwise they probably would have done so already using current possibilities like userland pipe constructs. And when they currently use method chaining, I'm sure they would feel quite at home with F#, where they simply switch from . to |>, whenever such libs emerge.

@Jopie64: Just to clarify…Are you positing that programmers using non-functional programming styles would not find Hack pipes useful for flattening their deeply nested expressions?

Hack pipes are designed to be (hopefully) universally useful for all programming styles, including traditional styles—because all programming styles, including traditional styles, involve deeply nested expressions. (Indeed, all of the real-world examples in the explainer are currently based on real-world traditional APIs that are not focused on higher-order functional programming, except for the examples from Ramda’s API (cf. #218).

and, as F# suggests, tacit programming should be kept internal anyway

You use this argument more often, but I still think it's a misleading one. Either F# makes an exception for |> or doesn't consider this as tacit encouraging programming, because |> is used and encouraged extensively there. I think it's the latter. (I know I also keep making the same argument:) technically you could consider F# pipes point-free encouraging because you create a function without naming the final 'point'. But semantically the final point is mentioned directly before the |> instead of after. It is true however that the declaration of the function you are using on RHS must be so that the final parameter can be curried. And here lies the difference in F# and JavaScript, in F# usually all parameters are curried where in JavaScript it is easy to do but, you could argue, unnatural. Still, I think, the 'tacit encouraging' argument is misleading. I'd say it is currying encouraging.

@Jopie64: This is fair enough, and this argument is well taken. It is true that, although pervasive tacit programming is discouraged by F#’s documentation, |> is pervasive in F#. As you say, it’s seen simply as an inverted function-application operator that can be chained.

The difference is, as you point out, the fact that all functions are automatically curried in F#. And automatic function currying is not built into the JavaScript language, and most functions in JavaScript code are not curried. In JavaScript, curried functions are yet another color of function that have to be used in a special way…presumably with a yellow color 🍛. Although some people like using this color, there are many representatives on TC39 (including outside this pipe champion group) who have pushed back against encouraging currying and partial application.

I would amend (and hopefully strengthen) my argument thus: although F#’s documentation does not discourage the pervasive use of its |> operator, F#’s documentation does discourage pervasive use of partial application, and it brings up several reasons why (like cognitive load and debugging). And I think that these reasons against pervasive partial application (cognitive load and debugging) still do apply to JavaScript, insofar that F# pipes would encourage the declaration of many curried or otherwise partially applied functions.

(Though note again that I am in favor of a partial-function-application syntax and an F#-style pipe in addition to Hack pipes. And I plan to try to fight for a PFA syntax and F# pipes after Hack pipes. But it has always been an uphill battle against many other representatives’ reasonable challenges. I have hope that someday we will get PFA syntax, but the hope is small.)

Another thing is: Of all arguments I keep reading in favor of hack, I am missing an in my eyes quite compelling one that I only read in the HISTORY.md file: A concern that it might encourage users to create even more performance degrading closures.

And although I think they really have a point there, I also think that (although I'm not a compiler expert) this can be optimized later. E.g. when such a function is only used in |>, and the curried 'intermediate' function does nothing but returning a function requiring the last argument, I'd say that creating a closure can be optimized away by doing as if the function is not curried, since |> immediately calls the just created closure anyway.

@Joepie: There are several strong concerns that TC39 representatives outside of the champion group have brought. Performance is indeed one of them, although it is not the only one. The browser implementers have said repeatedly to the group that people generally overestimate how much optimization they can do, and they feel that it probably applies to this case (e.g., if a curried function is declared separately—also observable error stack traces getting in the way of inlining). But I think this performance discussion is kind of off topic for this issue. It probably should move to #221.

Edit: Oh, wait, you already brought it up in https://github.com/tc39/proposal-pipeline-operator/issues/215#issuecomment-922259832. That’s fine; it’s sort of related. 😄

Anyways, thank you for your erudite and intelligent comment. It also will definitely be important for improving the explainer, too.

js-choi avatar Sep 19 '21 00:09 js-choi

@js-choi First of all, thank you for opening the issue of this aspect and I also appreciate your hard work (must be, I guess) for Brief history of the JavaScript pipe operator that I think the information listed is extremely valuable for everyone who concerts the matter.

For the "Impact of Hack pipes on functional programming", I would like to paraphrase "Impact of Hack pipes on algebraic structure in JavaScript".

@stken2050: Thanks for your comments, and I am glad you found HISTORY.md to be informative.

To be honest, I kind of find the following comments about algebra confusing. The Hack pipe is a pure binary operator with special evaluation rules (just like how => is a binary operator with special evaluation rules). The Hack pipe, just like =>, is by itself referentially transparent, but it (again, like => and every other JavaScript operator) may contain an expression that itself performs side effects. the Hack pipe do not introduce more side effects; the Hack pipe is a pure operator; it binds a constant free variable, which is a pure operation, just like => and the lambda calculus). The Hack pipe does not change the meaning of =—but (just like => and every other JavaScript operator), the Hack pipe may contain an assignment operator or a function call that performs a side effect.

Hopefully that makes sense. But this is all off topic. This thread is about the impact of the Hack pipe on the JavaScript functional-programming ecosystem. My apologies for marking your comments as off topic—I’m marking this comment as off topic too.

If you’re still concerned about side effects or referential transparency, then I would encourage you to create new issues devoted to this. 😄

js-choi avatar Sep 19 '21 01:09 js-choi

My opinion on this is based on an idea that I've seen floating around in other thread about the "tax" that comes with each style. People will often take the easier path and by taxing every call equally with some variant of (^), hack style doesn't incite toward extracting the state or not. On the other had, based on our previous discussions, F# style should incite or at least might incite more people to use curried functions. These functions aren't taxed as much as multi arguments functions in F# because you do not need the x => part at all.

Basically, over time, you would expect to see a lot more code like this: value |> a(1) |> b(2) |> c(3) than like this: value |> x => a(1, x) |> x => b(2, x) |> x => c(3, x). This makes lambdas, and thus opportunity to extract the state, a lot more visible in F# style. At least, assuming a world where currying takes on. If it doesn't, then this whole thing is not going to matter.

@kawazoe: This is a well-reasoned comment.

Standardizing F# pipes alone would encourage quite different patterns from what Hack pipes (even with F# pipes) would encourage. They would encourage many curried functions.

Lots of people have strong opinions about which approach is better to encourage. It is certainly true that, in many statically typed functional programming languages, automatic currying might encourage certain good habits. (I myself am a huge fan of F#, and I think several aspects of Dan Syme et al.’s design are ingenious.) But JavaScript differs from them in many fundamental aspects (https://github.com/tc39/proposal-pipeline-operator/issues/215#issuecomment-922361733 has some good points about some important impedance mismatches between statically typed FPL and JavaScript).

In any case, as you know, several TC39 representatives (not only inside but also outside of the pipe champion group) unfortunately have strong concerns about encouraging F# (see HISTORY.md, #221, and https://github.com/tc39/proposal-pipeline-operator/issues/215#issuecomment-922178037).

Many of these concerns (particularly from engine implementers) include memory performance. But many of these concerns also include ecosystem siloing/forking.

We the pipe champion team are hopeful that Hack pipes would actually help increase interoperability between functional APIs and non-functional APIs. We’re hopeful that this increased interoperability would decrease siloing, rather than increasing it.

But we understand that many people used to tacit pipe functions may be skeptical of this prediction. And I accept that this may indeed be partially motivated reasoning reached after F# pipes and PFA syntax ran into their walls (#221)—because in the end we cannot predict for sure what will happen. (However, I hope to improve examples of interoperability between curried functional APIs and traditional/web APIs—see #218).

In any case, we still need to document these discussion points in our explainer. Thank you for the well-reasoned comment.

js-choi avatar Sep 19 '21 01:09 js-choi

I think all of this discussion can be reduced if we just accept the smart mix option the new hack pipe will be accepted the js functional programming ecosystem does not change anything and people won't use currying so much when there is hack type. @js-choi is there any way that the smart mix option will be discussed one more time?

mohaalak avatar Sep 19 '21 04:09 mohaalak

@mohaalak: This is somewhat covered in #221 and https://github.com/tc39/proposal-pipeline-operator/issues/202#issuecomment-918595019. Like #221 talks about, F# pipes and smart-mix pipes have run into a lot of deep concerns from TC39 representatives outside of the pipe champion group since 2017. There is almost no way that smart-mix pipes (or F# pipes) would pass through TC39 in the foreseeable future (or—hopefully not—ever). I personally plan to fight for F# pipes and PFA syntax in the future, but trying to add them now would probably kill any prospect of TC39’s representatives agreeing about any pipe operator.

This is off topic, though, so I’d like to direct you to #221 and #202. I’ll mark my own comment as off topic, too. (My apologies!)

js-choi avatar Sep 19 '21 04:09 js-choi

@js-choi

I thought I was invited to express my opinion in your new threads from the previous thread. That is what you told us.

It turns out you guys still keep closing around posts as offtopic. I took my time to post my opinion so as yours, and ok, where is the adequate on-topic for https://github.com/tc39/proposal-pipeline-operator/issues/217#issuecomment-922358825 ?

If there is not, I will start a new one for this, Is it fine?

ken-okabe avatar Sep 19 '21 04:09 ken-okabe

@js-choi Is there a reason why the champions don't leverage the State of JS survey and surveys at the most popular Web platform conferences to explicitly elect feedback from the community regarding the pipeline? TC39 members have done informal JS surveys that consistently has pipeline without a doubt one of the most in-demand features.

I think it only makes sense for that to be done for more informed decisions to be made than the whataboutisms and ancedotal experiences masked as facts from various sides of the direction this standard should go. I remain unconvinced hack-style with no notable traction in comparison to F#-style before, during, and after its introduction towards advancing in the proposal process is the way to go for use cases like chaining with its current semantics.

Simultaneously, anecdotal accounts on my part, the current champions, and others need to be deprioritized with actual quantitative data from the JS community.

//cc @mAAdhaTTah @tabatkins @rbuckton

lozandier avatar Jun 17 '22 12:06 lozandier