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

Separate (complementary) F# pipeline proposal?

Open mmkal opened this issue 4 years ago • 51 comments

As someone who was firmly in the F# and partial application camp, I'm very interested in the teeny section at the end of the readme called Tacit unary function application. Between that and +> I think bases would be really well covered. I don't feel that way about Hack alone, but still think it's fantastic that some form advanced to Stage 2.

Is the plan to include |>> in this same proposal? As a separate one with the same champion(s)? Is there anything that community members can do to maximise its chances (ideally at the same time, so implementers can handle both together)?

mmkal avatar Sep 11 '21 01:09 mmkal

What you're referring to is the Proposal 3 - Split mix - it is the better option to the Proposal 2 - the current Hack proposal.

It is, however, still flawed, because there is an even better option:

F# + The partial application proposal:

  • https://github.com/tc39/proposal-partial-application
  • https://github.com/tc39/proposal-pipeline-operator#hack-pipe-functions

With it (partial application + F# pipes), you get the same functionality as you'd get with Hack, but 1) without the need of an additional operator |>>, and 2) the partial application functionality would work outside the scope of pipeline operators (meaning the whole language), which is great, as opposed to the special token of Hack that only works in the context of pipeline operators, 3) all other benefits of F# over Hack.

example - F# + partial application:

const multiply = (x, factor) => x * factor;

[1,2,3]
 |> multiply(?, 2)

// which just de-sugars to:

[1,2,3]
 |> (temp1) => multiply(temp1, 2)

it looks the same as Hack, and indeed is the same as Hack! What it does is curries out the argument marked with ?, and the F#'s pipeline operator calls that curried function.

But, while being the same, this proposal has an added benefit that the partial application operator would work anywhere in the language:

const multiply = (x, factor) => x * factor;

// can use directly - just like Hack:
[1,2,3]
 |> multiply(?, 2)

// can create a curried function
const multiplyBy2 = multiply(?, 2);

// and can use it the FP way, without needing an extra operator `|>>`:
[1,2,3]
 |> multiplyBy2

[1,2,3]
 .map(multiplyBy2)

// everyone is happy!

which is, as already mentioned, considerably better than the Hack + |>> proposal.

Why was it not considered? I don't know. In my view, this is the best that can happen with pipeline operators, meanwhile what we currently have in Stage 2 is the worst - even worse than no pipeline operators at all.

Why was Hack + |>> not considered (the proposal 3)? See [1]. Apparently it's because it's 2 operators and no browser vendors want to implement it so nobody championed it, which seems ridiculous because with Hack we're adding 2 operators too, and 3 if we also include |>>.

This is why I feel the whole thing is rushed. One part of why TypeScript ended up so good is because the guy who designed C# also participated in designing TypeScript. With pipeline operators in JS, it becomes more and more obvious to me that there were simply not enough FP-competent people in the TC39 committee, especially from different languages outside JS like Haskell, F# etc. - if there were, there's no way we would've ended up choosing the Hack proposal, especially without |>>.

And some are saying that JS is not necessarily an FP language. I disagree. History repeats itself. The same thing has happened many years ago, when NodeJS chose to use callbacks instead of promises. This is one of the regrets of Ryan Dall, the creator of Node. The arguments were the same as we're having right now, the reasons behind it were the same too. Let's not make the same mistake again.

And even then, with the F# + partial application proposal, nobody's forcing FP down your throat, because you get to choose yourself. This is not the case with the currently Hack proposal, and it is the case with Hack + |>>, though worse because of reasons alreay mentioned.

[1] https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application

kiprasmel avatar Sep 12 '21 12:09 kiprasmel

Yes, I'm essentially talking about reviving Split Mix, now that Hack pipes has achieved consensus as a direction from the committee.

The line:

Requires browser vendors to agree to implement two similar pipe operators, so nobody is currently backing this proposal

Was written quite a while ago, and I'm hoping things have changed sufficiently that it can be revisited. Specifically, without consensus on one operator, I can see why it would seem risky to put all eggs in a two-operator solution basket. Now that Hack pipes have consensus, hopefully the door is open to at least trying to advance another operator. If it fails, or is delayed, Hack could advance ahead of it - that would be a shame but still better than nothing.

@kiprasmel I don't want to get into debate about Hack vs F# here either (partly because I personally also would have preferred F#). The premise of this particular GitHub issue is acceptance of Hack as the "winner", the topic is whether F# pipes should be a separate proposal or not. As I suggested in the other thread, maybe you should make a new issue in which you can try to make the argument for abandoning Hack altogether. It's off-topic here.

mmkal avatar Sep 12 '21 13:09 mmkal

@mmkal thank you, good point, I will make a separate issue.

Though, currently it seems like the trade-offs are being discussed in a gist outside this repo, by a TC39 member who is opinionated towards Hack - I'm not a fan of it being outside this repo:

https://gist.github.com/tabatkins/1261b108b9e6cdab5ad5df4b8021bcb5

Update: see #205

kiprasmel avatar Sep 12 '21 14:09 kiprasmel

It would be good to have separate places for discussion of hack and F#-style proposals.

I believe that earlier hack-style was being discussed at https://github.com/js-choi/proposal-hack-pipes/

Should F#-style proposal related discussions happen at https://github.com/valtech-nyc/proposal-fsharp-pipelines instead? Why is there no link to that in the readme anymore?

Is there no one who's championing the F#-style proposal?

peey avatar Sep 13 '21 01:09 peey

@peey interesting, I didn't know about that fork. It looks like it hasn't been updated in a while, and if it's to be made complementary to this proposal (which is, at time of writing, Hack), it should remove all special cases for await and change the operator to |>>. It might be easier to write a new one because |>> is so simple - everything messy can be handled by |>. It looks like @mAAdhaTTah was involved in it - any thoughts?

mmkal avatar Sep 13 '21 12:09 mmkal

@mmkal If a separate F# proposal is desired, you should create a new repo for it. That repo was for the competing F# syntax before Hack advanced to Stage 2.

mAAdhaTTah avatar Sep 13 '21 12:09 mAAdhaTTah

@rbuckton I'd like to respond to your comment in https://github.com/tc39/proposal-pipeline-operator/issues/91#issuecomment-917645179 since it directly relates to the topic of this issue:

I have been a staunch advocate for F#-style pipes since the beginning, but it's been difficult to argue against the groundswell in support of Hack-style pipes due to some of the limitations of F#-style pipes. If we kept debating on F# vs. Hack, neither proposal would advance, so my position as Co-champion is "tentative agreement" with Hack-style. Both sides have advantages and disadvantages. F#-style feels to be a better fit for JS to me, especially given the existing ecosystem, and meshes better with data-last/unary producing libraries like Ramda and RxJS. F#+PFA works well for data-first libraries like underscore/lodash. However, F#-style is harder to use with yield, await, and methods where the topic is the receiver (i.e., ^.method()), which is an advantage for Hack-style.

So we were stuck at impasse. I don't think a solution that mixes both is tenable, and I'd venture to guess that having multiple pipe operators (one for each) may not make it through committee. As what feels like the lone dissenter against Hack-style on the committee (or at least, the only vocal dissenter specifically in favor of F#-style), I'd rather not stand in the way of the feature advancing at all, despite my reservations, because I think it's a valuable addition regardless of approach.

If we could find a way to get two pipe operators through committee, I would support that. JS wouldn't be the first language with multiple pipe-like operators. I've even mentioned up-thread that I'd support a Hack-style "topic pipe" like ^> or ||> or -> along side a F#-style function pipe like |>. That's a separate issue, however, and maybe we can discuss that outside of this issue as something to bring before committee.

Are there specific people (on the committee, or browser implementors) who are strongly against multiple operators? If so, maybe they could be invited to speak up here, since there is clearly huge interest in F#-style pipes (documented in #205) - so an offhand dismissal of multiple operators would be very disappointing: the use cases are real, clear, very popular and there's well-established precedent in other languages.

(A note about popularity - I agree that it would be a bad idea to just make decisions based on GitHub upvotes, but the volume, passion, and popularity of arguments in favour of F# is significant and shouldn't be ignored. Which is why even if there's some resistance to more operators, it's worth pushing IMO. The suggestion isn't one that's made capriciously.)

To make this more of a practical and direct question - @rbuckton as a committee member, do you think a new complementary proposal repo should be created so that this one can focus on Hack? CC @tabatkins @js-choi and @ljharb - would be interested to hear your thoughts on this too.

mmkal avatar Sep 13 '21 12:09 mmkal

A number of people on the committee are just barely on board with pipeline in general, and specifically found smart-mix to be too complex of a mental model to be worth supporting. I suspect that presenting the same two-version syntax via a pair of operators would bring up the same complaints. (This was never seriously advanced as an option during committee meetings, however, so I'm not 100% sure; this is my intuition only.)

In general, tho, speaking as a language designer with a decade+ of experience, having two distinct features that are only very slightly different is almost always a mistake. You usually want clear, bright lines separating features, so people know when it's appropriate to use each feature. Subtle differences tend to bring confusion instead; it's almost always the right choice to instead just choose one variant and let the other use-cases be slightly inconvenienced. (Or, sometimes, not doable at all; you want to avoid locking out use-cases as much as possible, but good design sometimes has to accept that as a tradeoff.)

Sometimes that's not the best choice, if there are multiple use-cases that are roughly equally vital and the inconvenience of doing one in the other's version of the feature is significant. But that's usually not the case, and definitely not here - as has been argued a number of times, even if you have a library built on returning unary functions, using them in a Hack-style pipeline just requires appending (^) to the end of the function.

So, as a champion I would be pretty strongly against a two-operator solution, where the two are precisely identical in powers, but just call their functions slightly differently.

tabatkins avatar Sep 13 '21 19:09 tabatkins

That said, if anyone wants to pursue any of these ideas on their own, I have neither the power nor the desire to stop you. ^_^

tabatkins avatar Sep 13 '21 19:09 tabatkins

I am personally pro-eventually-adding-a-split-mix tacit-unary-function-call |>>. I agree with @rbuckton (https://github.com/tc39/proposal-pipeline-operator/issues/91#issuecomment-917645179) that |>> is a reasonable goal.

JS wouldn't be the first language with multiple pipe-like operators.

However, @tabatkins is right in that the committee was barely supporting any pipe operator (there are still some delegates that argue for no pipe operator at all). That was much of the reason why any pipe operator was stalled for such a long time (before the State of JS 2021 results showed that lots of people requested a pipe operator). (I plan to add a history.md timeline to this repository within the next few days that talks about this in more detail.)

I’ll copy and paste some parts from what I said here: https://github.com/tc39/proposal-hack-pipes/issues/18#issuecomment-912661139

Whether to reintroduce split mix (|>>) and whether to do it now or later

The second thing to address is whether and when we should try to reintroduce what we have called “split mix”: a separate pipe operator |>> for tacit unary function calls. This would allow people using data-last / curried-function styles to omit (^).

I think @tabatkins has said before that also having |>> might be a hard sell to the Committee, because from JavaScript’s perspective x |> f(^) is equivalent to x |>> f under this paradigm […]. But |>> is something that I would be happy to work on…

…It’s just that the Committee may well balk if we do too much at once. It’s already having a hard time swallowing any pipe operator. It generally prefers to do things piecemeal, while keeping forward compatibility with future extensions. And Hack pipes are forward compatible with |>>.

My inclination (@tabatkins, @ljharb, @mAAdhaTTah, others are free to insert their own opinions) therefore is to keep this first pipe proposal focused on Hack |>, and to leave |>> to a later proposal (if |> ever even gets accepted, as we hope).

(Aside: I would like to push back at “these FP libraries represent the majority of the pipe function usage that the pipeline operator is supposed to replace”. Although unary function calls are a significant use case, from this proposal’s perspective, unary function calls are not the most common use case. The pipe operator is not supposed to replace only unary function calls; it is supposed to replace deeply nested expressions in general, which occur in all APIs. In the worst case, people using Ramda/RxJS/etc. can keep using pipe functions for curried-function calls…while still finding Hack pipes useful for interoperation with other APIs’ data-first function calls, function calls with trailing option objects, Web APIs, and so on.)

Anyways, I’m personally enthusiastic about |>> for tacit unary function calls and, in fact, I’d be eager to eventually write a proposal for |>> myself. (Clojure is one of my homes, and Clojure has a ton of threading macros, so I’d be used to having two pipe operators.) It’s just that the Committee may recoil from too big a bolus of syntax at once…It’s already been hesitant at even one pipe operator. If there’s enough of a use case for |>> (e.g., working with TypeScript’s limited type unification before microsoft/TypeScript#30134 lands), then it can fight for it on its own merits. I would be happy to try to make that fight for it, later.


I will say that partial function application may definitely coexist with Hack pipes—either in the form originally proposed by @rbuckton (which has eagerly evaluated partial arguments, once before any function calls) or as Hack pipe functions (which are like arrow functions in that the partial arguments are evaluated lazily, at each function call).

I would be happy to fight for a partial-function-application syntax later, too.

js-choi avatar Sep 13 '21 21:09 js-choi

@tabatkins @js-choi thank you - that's very helpful context.


A number of people on the committee are just barely on board with pipeline in general

Hmm. I'd be interested to learn more about this - are there any specific meetings that it'd be worth reading to understand this better in https://github.com/tc39/notes?

In general, tho, speaking as a language designer with a decade+ of experience, having two distinct features that are only very slightly different is almost always a mistake

There are many counterexamples to this in successful and popular languages, including F# which has |>, >>, <|, <<, as well as the ability to call functions like add 1 or add(1).


Also, I don't think this is true:

EDIT: THIS EXAMPLE IS WRONG

even if you have a library built on returning unary functions, using them in a Hack-style pipeline just requires appending (^) to the end of the function

const {memoize} = require('lodash')

const track = value =>
  value
    |> ^.toLowerCase()
    |> memoize(trackEvent)(^)

Behaves differently from this:

const {memoize} = require('lodash')

const track = value =>
  value
    |> ^.toLowerCase()
    |>> memoize(trackEvent)

In the first track('FOO'); track('foo'); will lead to trackEvent being called twice. In the second, once. In the case of memoize, it's probably the second one that I want. In other situations, it might be the opposite. Of course, there are workarounds, but it's a bit of a footgun. And the workarounds aren't always appropriate (above, it might not be desirable to add a const memoizedTrack = memoize(trackEvent) to the scope). This is much more than a three-character tax, the actual runtime semantics are different.


Another case, where it is just tax, but it's more significant than (^) at the end:

value
  |> ^.toLowerCase()
  |>> JSON.parse
  |>> ({ x, y, z }) => {
    const api = new SideEffectyAPI(x)
    api.initialize(y)
    return api.foo(z)
  }

vs

value
  |> ^.toLowerCase()
  |> JSON.parse(^)
  |> (({ x, y, z }) => {
    const api = new SideEffectyAPI(x)
    api.initialize(y)
    return api.foo(z)
  })(^)

Needing to wrap the arrow function in parens is a heavy, confusing, multi-line tax.

Granted, SideEffectyAPI here is probably not well designed, but it might be an external library that I can't control.

Note: do-expressions might mitigate this problem somewhat.

Lastly, even under "normal" circumstances the tax is sometimes just... lame. And there will always be some data-last unary functions. How weird and wrong the following looks will be a disincentive to using language-level syntax for pipelines and will lead to deep schisms, instead of encouraging almost all JS users to write in a similar way. Weird and wrong-looking example:

getUser()
  |> Either.fold(
    err => `Something went wrong getting your user info. ${err.message}`,
    user => `Hello ${user.name}`
  )(^)
  |> console.log(^)

I suspect FP users will just use their user-defined pipe functions, and other users won't use FP libraries' utilities, when it doesn't have to be that way.


That said, if anyone wants to pursue any of these ideas on their own, I have neither the power nor the desire to stop you. ^_^

Err... aren't you a committee member? Doesn't that mean you do have the power, since stage-advancement requires 100% consensus? Good to know that you don't have the desire, in any case!


@js-choi good to hear! It'd be great to know what you think the right timing would be.

mmkal avatar Sep 14 '21 02:09 mmkal

In general, tho, speaking as a language designer with a decade+ of experience, having two distinct features that are only very slightly different is almost always a mistake.

@tabatkins Does that mean the adoption of Hack-style would decrease the chance of partial application reaching Stage 4? If so then I feel that the Hack-style proposal should be evaluated against both F# and partial application rather than F# alone. I know that's not the norm for the TC39 process, but this proposal has been anything but ordinary.

Is there any chance that the committee would consider evaluating the pros/cons of advancing Hack-style against advancing both the F# and partial application proposals before any reach Stage 3? I think it would be helpful to consider if advancing both F# and partial application to Stage 2 simultaneously may be better for the JavaScript language than either pipeline proposal alone.

I also think doing so in earnest would help alleviate some of the contention around the pipeline operator in general regardless of the outcome. 😀

sandren avatar Sep 14 '21 02:09 sandren

@sandren no, partial application has a lot of obstacles to work through on its own, and pipeline advancing as F# wouldn't help it much.

ljharb avatar Sep 14 '21 02:09 ljharb

good to hear! It'd be great to know what you think the right timing would be.

Does that mean the adoption of Hack-style would decrease the chance of partial application reaching Stage 4? If so then I feel that the Hack-style proposal should be evaluated against both F# and partial application rather than F# alone. I know that's not the norm for the TC39 process, but this proposal has been anything but ordinary.

And at the cost of a pipeline operator that will provide something new, and I assume at the cost of the partial application proposal.

I personally would support a syntax for partial function application.

I don’t think that the adoption of Hack pipes would decrease the chance of a PFA syntax. They can coexist.

However, I also think any syntax for partial function application (which I personally do support) would continue to run against pushback at the committee, regardless of what the pipe operator happens to be. Like @ljharb said, F# pipes advancing actually wouldn’t help PFA syntax that much.

So, although I’m somewhat hopeful for the future of PFA, I also have tempered expectations. This has little to do with Hack-vs.-F# pipes and more to do with other TC39 delegates’ concerns about PFA in general (like performance and aesthetics).

I’ll paste what I said in https://github.com/tc39/proposal-pipeline-operator/issues/207#issuecomment-918596951:

For what it’s worth, I think that partial function application could coexist with Hack pipes—either in the form originally proposed by @rbuckton (which has eagerly evaluated partial arguments, once before any function calls) or as Hack pipe functions (which are like arrow functions in that the partial arguments are evaluated lazily, at each function call).

I’d be happy to fight with @rbuckton for a partial-function-application syntax later. But it might be an uphill battle (and not because of Hack pipes). From what I recall, some people on the committee have always been against partial-application syntax (independently of pipe). I’m not one of them, but…

So, pushing for partial function application should probably wait, until some pipe operator would get further. The Committee can stomach only so much new syntax at once.

js-choi avatar Sep 14 '21 02:09 js-choi

@mmkal

const {memoize} = require('lodash')

const track = value =>
  value
    |> ^.toLowerCase()
    |>> memoize(trackEvent)
track('FOO'); track('foo');

Could you please explain how this would end up calling trackEvent only once? I can't see it.

lightmare avatar Sep 14 '21 07:09 lightmare

why are we even bothering to discuss this? Tab will lock this out, just like they did with #205.

There's no point on focusing on the proposal itself right now - let's first solve the corruption issue maybe?

https://github.com/tc39/how-we-work/issues/96

kiprasmel avatar Sep 14 '21 10:09 kiprasmel

EDIT: THIS IS WRONG

@mmkal

const {memoize} = require('lodash')

const track = value =>
  value
    |> ^.toLowerCase()
    |>> memoize(trackEvent)
track('FOO'); track('foo');

Could you please explain how this would end up calling trackEvent only once? I can't see it.

@lightmare my apologies, you are totally right. Maybe this example makes more sense.

const logSlowResponses = threshold => value => {
  if (Date.now() > threshold) {
    console.log('slow response!', value)
  }
  return value
}

value
  |> fetchFromSlowAPI(^)
  |> logSlowResponses(Date.now() + 3000)(^)

👆 will never log whereas using |>> we can make sure it throws when the API was too slow, as expected:

value
  |>> fetchFromSlowAPI
  |>> logSlowResponses(Date.now() + 3000)

mmkal avatar Sep 14 '21 12:09 mmkal

@mmkal I still don't get it. For the second example to log "slow response!", you need Date.now called before fetchFromSlowAPI.

lightmare avatar Sep 14 '21 12:09 lightmare

Is this kind of what you're trying to express?

const pipe = (input, ...funcs) => funcs.reduce((x, f) => f(x), input);

pipe(
  value,
  x => fetchFromSlowAPI(x, xyz),
  logSlowResponses(Date.now() + 3000),
);

lightmare avatar Sep 14 '21 12:09 lightmare

EDIT: THIS IS WRONG

@lightmare yes, that's basically it. I think I got my example wrong again. I've updated my example to consistently use |>>. That is, I changed |> fetchFromSlowAPI(^, xyz) to |>> fetchFromSlowAPI (since the xyz was made up anyway).

So, the updated example is

value
  |>> fetchFromSlowAPI
  |>> logSlowResponses(Date.now() + 3000)

Since |>> is just an operator which is operating on functions, logSlowResponses(...) is called before fetchFromSlowAPI(...), and the curried function return value logSlowResponses(...)(...) is called after.~

mmkal avatar Sep 14 '21 12:09 mmkal

@mmkal Ok thanks, now I understand. It won't work that way, though:

Since |>> is just an operator which is operating on functions, logSlowResponses(...) is called before fetchFromSlowAPI(...), and the curried function return value logSlowResponses(...)(...) is called after.

Every binary operator in JS evaluates its operands left-to-right.

The F# pipeline operator has to be left-associative. x |>> f |>> g is equivalent to (x |>> f) |>> g, but x |>> (f |>> g) is something completely different.

Going back to your example:

value
  |>> fetchFromSlowAPI
  |>> logSlowResponses(Date.now() + 3000)

The order of evaluation is:

topic = value
topic = fetchFromSlowAPI(topic)
topic = logSlowResponses(Date.now() + 3000)(topic)

lightmare avatar Sep 14 '21 13:09 lightmare

value
  |> ^.toLowerCase()
  |> JSON.parse(^)
  |> (({ x, y, z }) => {
    const api = new SideEffectyAPI(x)
    api.initialize(y)
    return api.foo(z)
  })(^)

I through this is probably better handled by do expression proposal instead. Juts like F# pipe + partial function Hack pipe + do expression is probably another good combo at preserve proper reading order

value
  |> ^.toLowerCase()
  |> JSON.parse(^)
  |> do {
    let { x, y, z } = ^
    const api = new SideEffectyAPI(x)
    api.initialize(y)
    api.foo(z)
  }

BTW.

The kotlin actually allow similar usage I stated above (mixing statement and expression in a long chain). The above can actually be written in valid kotlin with minor change.

value
  .let { it.toLowerCase() }
  .let { JSON.parse(it) }
  .let { (x, y, z)->
    val api = SideEffectyAPI(x)
    api.initialize(y)
    api.foo(z)
  }

(I am not saying it is a good or bad practice to write wildly long chain. Just say there are already some languages allow so)

mmis1000 avatar Sep 14 '21 14:09 mmis1000

I through this is probably better handled by do expression proposal instead.

See also tc39/proposal-hack-pipes#4 (which led to me rewording some stuff in the explainer a few months ago).

js-choi avatar Sep 14 '21 14:09 js-choi

Yup, a few people have addressed it now, but I'll stress again that val |> foo(^) |> bar(^) (Hack-style) and val |> foo |> bar (F#-style) are precisely identical in every observable aspect of their execution - timing, ordering, etc.. You need to examine the AST to tell they're different.

The confusion is likely stemming from the fact that both are different from val.pipe(foo, bar), because functions evaluate all of their arguments before beginning execution. So in this case, the foo and bar expressions are both evaluated before you start piping to either of them, while in F#-style the foo expression is evaluated, then piped to, then when it returns the bar expression is evaluated.

This is the same as foo() + bar() + baz() - foo() and bar() are evaluated, then they're added together (potentially invoking their .valueOf() or .toString() methods, which can be observable and/or slow), then baz() is evaluated and added to the result. Operators work a little bit differently than functions, is all.

tabatkins avatar Sep 14 '21 15:09 tabatkins

An example showing off the above:

function foo(v) { 
	console.log("call " + v); 
	return {
		valueOf: ()=>{
			console.log("value of " + v); 
			return v;
		}
	}
}

foo(1) + foo(2) + foo(3)

// logs:
call 1
call 2
value of 1
value of 2
call 3
value of 3

tabatkins avatar Sep 14 '21 15:09 tabatkins

Thanks @lightmare and @tabatkins - I actually didn't know that about +, and it's definitely a good thing that operators work that way. Sorry for the confusion/bogus example. I've annotated my comments.

Still, I think the "tax" is significant, since it looks wrong enough that it will put people off using it, leading to siloing of the community (rambda/rxjs/fp-ts users preferring their own pipe implementations). Which, as we've seen with how heated this repo's issues have gotten, is harmful and counter-productive.

mmkal avatar Sep 14 '21 17:09 mmkal

@mmkal: Yeah, those concerns about siloing the JavaScript tacit-programming community are understandable. I’m sorry for all the frustration that people who like tacit programming have had—feeling like something was promised but taken away. (See https://github.com/tc39/proposal-pipeline-operator/issues/206#issuecomment-918609518.)

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.

And we think 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.

And, in the future, although it faces many barriers, I might try to propose a tacit F# pipe operator too and/or help the partial-function-application syntax (https://github.com/tc39/proposal-pipeline-operator/issues/202#issuecomment-918595019). However, I know that some implementors and others on TC39 are skeptical of widespread tacit programming, and this would be an uphill battle—but it would be even with F# pipes; those concerns are separate from Hack pipes.

js-choi avatar Sep 14 '21 17:09 js-choi

Something just feels very wrong about this whole situation.

The FP JS/TS community has been eagerly awaiting and advocating for the pipeline operator for years, but the official TC39 position seems to be:

"the pipeline operator isn't for you; please use your pipe() function"

I don't understand. What does it even mean for a pipeline operator proposal to not facilitate functional programming?

Think about if the opposite situation were true and instead there was a Hack-style proposal for years with little interest and a new F# proposal emerged with a groundswell of community support, wouldn't it be the F# proposal that advances?

But in reality it feels like the original intent and enthusiasm for the pipeline operator is being used to advance an alternate proposal that wouldn't reach Stage 2 on its own merit.

sandren avatar Sep 14 '21 23:09 sandren

Yeah I was super upset because it felt like I had been bait and switched, which I am confident was not their intent but when it was proposed as Pipes; I was thinking javascript pipes, function pipes. I didn't even know hack existed before this proposal. I'm sure your language is awesome, and maybe I'll give it a try after all this, but if a javscript dev is excited about pipes, they are probably referring to the thing they are currently using called pipe in javascript and not a different thing. It's been genuinely a confusing and upsetting week but I'm trying to keep my chin up.

voronoipotato avatar Sep 14 '21 23:09 voronoipotato

@js-choi yeah, I do agree that Hack-pipes will decrease siloing somewhat, I'm personally not in any way suggesting they Hack pipes are bad (and anyone who is should use a different issue to say it). It's just a concern that they don't go far enough. Luckily the current form is totally forwards-compatible with |>> (and +>). They will work together really nicely!

My prediction: Hack pipes alone will increase interoperability a bit, but there will still be big silos full of the people who are objecting so forcefully to Hack, and those who agree with them but aren't into GitHub flamewars. Basically it's not just a tax of three characters on each step (more than three if a multi-character topic is chosen), it's the harm that will be done by subsets of the community avoiding that tax.

mmkal avatar Sep 15 '21 16:09 mmkal