Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frustrations of people looking forward to tacit syntax #215

Open
js-choi opened this issue Sep 18, 2021 · 42 comments
Open

Frustrations of people looking forward to tacit syntax #215

js-choi opened this issue Sep 18, 2021 · 42 comments
Labels
documentation Improvements or additions to documentation question Further information is requested

Comments

@js-choi
Copy link
Collaborator

js-choi commented Sep 18, 2021

Many people feel that they promised a syntax for tacit programming and then had that taken away. (They can still use user-land pipe functions, of course, but they were looking forward to tacit syntax, baked into the language.)

The lead-up to Hack pipes was four years in the making (ever since F# pipes failed to reach TC39 consensus in 2017: see #221 and HISTORY.md). As I said in #221, the champion group believes that F# pipes have met with far too much resistance from multiple representatives of TC39 to be able to pass TC39 in the foreseeable future (or even ever—though I hope not).

But the explainer currently doesn’t acknowledge the real frustrations that people looking forward to a tacit-programming syntax have at feeling something promised was taken away.

I have written some comments about this topic in various replies (e.g., #202 (comment), #206 (comment)). It is likely that the frustrations of people will not be fully assuaged unless we are able to persuade TC39 to also add a tacit-function-application operator (about which I am enthusiastic but worried; see #202 (comment)). But, in the meantime, the explainer should respectfully acknowledge these frustrations.

(Concretely, we need to integrate those comments into the explainer. I am not sure whether adding them to § Motivation, creating an FAQ section, or both would be better. Comments would be welcome.)

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, after (and if) we succeed in standardizing Hack pipes—see #202 (comment). 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.

This issue is for discussing those understandable frustrations about the process and the feeling of loss about something that was felt promised. Please try to keep the issue on topic, and please try to follow the code of conduct (and report violations of others’ conduct that violates it to tc39-conduct-reports@googlegroups.com). Please also try to read CONTRIBUTING.md and How to Give Helpful Feedback. Thank you!

@sandren
Copy link

sandren commented Sep 18, 2021

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 think everyone here from the FP JS/TS community understands and accepts that risk and would greatly appreciate you continuing to advocate for us. The pipeline operator is very important to us and many of us feel it is better to wait indefinitely than to land the wrong spec.

So even if the overall chances of getting a "pipeline operator" goes down by fighting for F# + PFA (or reviving the minimal proposal), it's still very much worth it to us because the alternative is to be literally locked into using pipe() forever.

I am still hopeful that TC39 will come to understand that the minimal proposal can reach Stage 4 while any remaining concerns about F# pipes + PFA are worked out over time.

@js-choi Thank you for being our champion! 🥰

@js-choi
Copy link
Collaborator Author

js-choi commented Sep 18, 2021

Thanks for the thanks! 🙂

But I don’t want to give false hope either. I do want to emphasize that, right now, I and the pipe champion team are set on pursuing Hack pipes, because they are the most likely way that JavaScript will get any pipe ever (with its benefits of nested-expression flattening and linearization). (Like I mentioned in #221, many members of TC39 outside the pipe champion group, including engine implementers, have consistently pushed back against F# pipes, PFA syntax, and tacit programming in general.)

So we’re really not planning to switch to “F# pipes only” at any time: I don’t want to keep fighting losing battles against those TC39 members (who are outside the pipe champion group)—and I think Hack pipes (which address many of their concerns) actually would help everyone, too (#217).

If we are even able to push through Hack pipes through TC39, then I plan to fight for F# pipes and PFA syntax afterwards. But, looking at what several TC39, I am hopeful about F# pipes and PFA syntax happening someday, but I have to be realistic: those engine implementers and other TC39 members have serious concerns about them (#221).

Although we’ve been talking about this stuff since 2017, we’re going to continue to try to be really careful moving forward and advance this slowly. Nothing is going to happen soon. But that doesn’t mean we’re planning to switch to F# pipes. I’m sorry if I gave false hope at all that we’re planning to change our mind soon. I am enthusiastic about F# pipes and PFA syntax, but I want to tackle them after Hack pipes.

To make this back on topic: your frustration about not getting tacit syntax yet is valid, and we could have explained our decision making more thoroughly and gradually around the August plenary meeting. (I hope the HISTORY.md document helps with explaining the decision making.) The explainer should have anticipated that frustration and should have addressed that frustration. I plan to edit the explainer later to address this in an FAQ or something, so that’s what this issue is tracking.

@kawazoe
Copy link

kawazoe commented Sep 18, 2021

@js-choi First, thanks for giving us a (multiple?) place to talk about these issues. I've only recently taken noticed the whole deal with the previous allegations and, wow this thing deserve its own book...

Anyway, after reading the HISTORY.md file, I realise that i something still doesn't add up to me. There's a place where @syg mentions a performance issue related to closures and "the ease at which [developers] can allocate many many [of them]" in regard to memory use. On the surface, this seems legitimate but, in an average Angular project with NgRx, we allocate millions of closures through the use of effect chains and various other means. My last Angular projet was of such a scale and ran just fine on a 7 years old phone. I have to ask... what kind of scale are we talking about here? Is it even possible to write legitimate code (not a benchmark) that can expose this kind of memory pressure?

If we were taking about an app, this would for sure be considered as premature optimisation. Obviously, designing a language is an other story, but I fail to see how the PFA, F#-style or even Hack-style proposals would change any of this. In the end, isn't it the programmer's responsibility to ensure their app performs well on their target device? JavaScript already lets people create massive multi-megabyte SPAs with extremely heavy frameworks. Why is the language only now thinking about the performance implication of an optional feature that can easily be disabled with a linter if a project needs requires it? If we designed every language features around performance, high-level languages wouldn't be a thing today so I am surprised to see this as a major argument in the design of the pipeline operator. That being said, this is not the core of the issue for me.

Right now, the only publicly available mention of this problem I can find is an opinion in meeting notes. Like I said earlier, there is a lot of angst in the community around the choice of style for the pipeline operator and I have to say that building a case about a core feature an entire community have asked for a very long time based on opinions sure isn't helping with that. Obviously, I am skeptical about this, (and I can say that after talking about it with multiple senior devs, that I am not alone) and since this perf issue appears to be a turning point in the discussion, I would love if more details could be published around these concerns. Mostly, I would like to see real-world data pointing out to this issue actually causing problems, ideally in a way that cannot be refactored around. Even better, I would love to see data that demonstrate that languages with PFA or some forme of a pipeline operators correlate with people using more closures than necessary in their code. This kind of data would definitely back the concerns evoked previously.

Again, I don't think stirring conspiracies is useful in such a thread. I really just want to understand where this performance concern comes from and if it is actually legitimate.

EDIT: You just said "So when an implementer says that they have deep concerns, e.g., about F# pipes’ or PFA syntax’s memory problems, then we have to really pay attention to that". I think my post goes exactly toward this point. We should pay attention. Not blindly agree that it is an issue. Such outstanding claims should come with equally outstanding evidence.

People mentioned that we need more members of the community in TC39 and, while I agree that it would be beneficial, I think staying empirical would help tremendously with picking the best possible decisions in the future as well as maintaining credibility toward the community. I can say that right now, I know multiple people who think TC39 is a joke... and it only took a few days for this opinion to form. This is incredibly sad and disheartening and I sincerely hope that The Explainer will provide information that was not public up to this point to properly justify the decision of moving Hack-style to stage 2. A lot of people are under the impression that their point of view matters in these discussions. What you are saying in this thread is that, it isn't really the case. Implementers will always have a veto over the community, even when the community says "no, we prefer to wait". If this ends up being true, then "the worst case scenario would [not end up being] for one browser member to refuse to implement something that got standardized and fork the language". It will be the community forking the language and creating a CommunityJS to WASM compiler. (just a feeling from what I've seen around here in the last few weeks)

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Sep 18, 2021

Such outstanding claims should come with equally outstanding evidence.

This is not an outstanding claim. It's a valid concern raised by an engine implementor based on th fact that tacit style programming, which F# pipe encourages, requires currying/unary functions/HOF, so you end up creating a new function that then needs to be executed in the pipeline. It's this closure proliferation that raises concerns. This is based on his experience implementing an engine and it's why engine implementors have valued expertise to provide to the committee. If F# pipe advanced, it would be something that he'd want to look into in more depth, and it's not a problem with Hack because no closures are created; it's just an evaluation of a sequence of expressions.

I also don't think it's particularly respectful of someone's experience to attempt to delegitimize their opinion like this. If you're going to engage in a conversation in good faith, you have to accept that the choice between the two is one of tradeoffs, and denying those tradeoffs isn't an objective evaluation of the proposals. The goal of the committee is to honestly evaluate those tradeoffs and choose the set that makes the most sense for the language. They don't have the luxury of ignoring issues like this in making that decision.

It's fair to ask how much the performance characteristics of a given piece of syntax matters, but JavaScript performance can and has suffered as a result of language design decisions. Even if it's the developer's responsibility to handle performance, "can't be used in a hot path" is still a downside for F# pipe. It's best not to take this too lightly.

@kawazoe
Copy link

kawazoe commented Sep 18, 2021

@mAAdhaTTah

This is not an outstanding claim. It's a valid concern raised by an engine implementor based on th fact that tacit style programming, which F# pipe encourages, requires currying/unary functions/HOF, so you end up creating a new function that then needs to be executed in the pipeline.

No one here is questioning that closures wouldn't be required for F#-style pipelines to be useful in most scenarios. What I am questioning, however, is to what extent you would need to use it to create enough closures so that it becomes a problem. Like I said, we already create millions of closures in some of the projects I've worked on, and they work just fine on antiquated hardware. It is true that it isn't an issue with Hack style, but as it as been stated over and over again in this repository, it has issues of its own as well that needs to be considered, hopefully also backed with hard data.

I'm sorry if it feels disrespectful to you that I choose to disregard experience in favor of data, but this is the kind of opinion that would get you kicked out of any scientific faculty. Experience is supposed to guide you toward good and useful data, not replace it. In fact, most scientific advances did not happened with a Eureka, but with a "uh... that's weird...". This kind of stuff only happens because someone with experience asked a question and the answer turned out to be unexpected.

The first question we should ask here is: Is there any evidence, in any language, that the presence of a pipeline operator encourages closures over other solutions to a point where it becomes a performance issue, outside of a benchmark scenario? The second one we should ask is: Does this applies to JavaScript, a language that already have multiple libraries built like massive closure-fests, in a significant way?

Again, you claim that they "can't be used in a hot path", but you are arguing about closures here, not the F# style pipeline operator. Your argument also apply to any hot-path-defined functions, so should we disregard map and reduce as well on the grounds that they probably come with closures too? There are most definitely ways to use this operator without creating a closure on every call, just like there is with map and reduce. It just depends on your usecase. Speaking of which, people are already avoiding pipeline-like code (function chaining APIs, pipe functions, etc.) in tight loops, so what makes you think they would use a pipeline operator there? Similarly, we already have libraries like ramdajs that auto curries everything they expose. Doesn't that causes closure problems already? And what if it does? Does it really matter if it still works for the people using those if their code ends up that much more maintainable because the operator encourages consistent argument positioning? How do you even set performance against maintainability?

These are all very good questions that I would have never asked without the insight that came from Syg's expertise. Its just not the kind of stuff I think of every day, and yet, they are still just that: questions. They still need answers if you want to make a meaningful decision.

This is most definitely not ignoring the issue. It is proving its existence. It is respecting one's expertise to further our understanding of a topic. It's asking for more; for where this insight came from. Since a very large amount of people are borderline furious about the decision to move Hack-style to stage 2, I think it is fair to say that the community definitely feels like the advantages of the other proposals where taken lightly and that trade-offs where most definitely ignored. At this stage, who knows if performance really is an issue. All we have is a hunch with no sign of an actual reasoning behind this decision. There might be more, a lot more from where this came from, but the community does not have this information; and from what I see it clearly feels cheated.

In other words, what you said there is just an opinion and accusing people of "not respecting the expertise" or "not understanding that the committee must make trade-offs" does not bring anything to the table. All I asked is if there is anything quantifiable backing this decision... I guess you just confirmed there isn't...

@lightmare
Copy link

@mAAdhaTTah

... based on the fact that tacit style programming, which F# pipe encourages, requires currying/unary functions/HOF ...

Even though most of you have dismissed Elixir pipe as unfit for JS, it still proves that tacit style programming does not always require currying/unary functions/HOF.

... so you end up creating a new function that then needs to be executed in the pipeline. It's this closure proliferation that raises concerns.

The way I read this is that there's concern about proliferation of currying in general. Worry that curried libraries would become more common. For current and future users of curried libraries, the performance impact of tacit-style-enabling pipe operator would be exactly the same as Hack pipe.

If F# pipe advanced, it would be something that he'd want to look into in more depth, and it's not a problem with Hack because no closures are created; it's just an evaluation of a sequence of expressions.

When comparing F# vs Hack pipe (putting aside await/yield), we consider two kinds of right-hand-side:

  • unary function call, where they're identical
    • there is no difference between F# x |> foo and Hack x |> foo($)
  • anything else, where F# needs an arrow function (or some other form of creating a unary function)
    • some people say this closure can be optimized away
    • others say it's not always feasible
    • the Babel plugin shows that in simple cases this is easy to do before it gets to the engine — it inlines the arrow function's body into the expression

@Jopie64
Copy link

Jopie64 commented Sep 18, 2021

I read about the performance considerations only recently, from the HISTORY.md document. Maybe I missed it, but if this is such a big issue for 4 years already I wonder why it didn't came up before as a pro to hack pipelining which doesn't have this issue?

Still I think as was already stated:

  • People already create closures when using existing pipe style
  • Although I'm not a compiler expert, it can probably be optimized away in most cases

Even more so, I think it is harder to optimize away when using existing pipe() like constructs, because there all closures need to be created before they are called one by one. With the f# pipe syntax, when the closure is created, it is immediately called. So much so that I think you could optimize the closure creation away altogether.

Hence this actually can be used as an argument pro to F#. Because I think with hack, people would keep using the existing unoptimizable pipe() syntax.

@mAAdhaTTah
Copy link
Collaborator

Your argument also apply to any hot-path-defined functions, so should we disregard map and reduce as well on the grounds that they probably come with closures too?

No it doesn't. In [1, 2, 3].map(x => x + 1), the arrow function can be inlined by the engine. In [1, 2,3].map(add(1)), it can't. map / reduce doesn't encourage currying & the proliferation of closures in the same way because the latter usage is not that common except specifically in FP communities.

I also said:

If F# pipe advanced, it would be something that he'd want to look into in more depth

The intuition is this would cause issues; investigations would have to be done by the implementors to validate that intuition. This is typically done around Stage 3 because everyone on the committee is a volunteer and there isn't time or money to conduct significant studies for every proposal. This is why we rely on the experience & expertise of engine implementors, and when they raise a concern like this, that needs to be taken into account. It's only one of several arguments in favor of Hack pipe, and not even close to the most significant, but it's still a valid concern about F#.


unary function call, where they're identical

This is the case where they're not identical:

// F#
x |> add(1)
// Hack
x |> add(1, ^)

add has to be evaluated to return a new unary function, then that intermediate function needs to be evaluate by the pipe. By contrast, the second version doesn't need to create an intermediate function.

@lightmare
Copy link

lightmare commented Sep 18, 2021

This is the case where they're not identical:

// F#
x |> add(1)
// Hack
x |> add(1, ^)

add has to be evaluated to return a new unary function, then that intermediate function needs to be evaluate by the pipe. By contrast, the second version doesn't need to create an intermediate function.

You're comparing apples to oranges. Those are two different add functions.

  • unary function call
// F#
x |> curryAdd(1)
// Hack
x |> curryAdd(1)($) // identical
  • anything else
// F#
x |> (y => binaryAdd(1, y)) // needs optimization
// Hack
x |> binaryAdd(1, $) // simpler

edit: typos

@mAAdhaTTah
Copy link
Collaborator

You're comparing apples to oranges. Those are two different add functions.

Yes, they are, and F# encourages the add(1) style, which will likely need to allocate intermediate closures. This is the performance concern with F# pipes.

@shuckster
Copy link

Performance vs. Longevity of a language feature

When performance concerns are raised, are yearly advances in hardware/compilers also taken into consideration? I accept that the future is by no means certain, but it's hard not to see JavaScript as having enough years ahead of it that it should seriously affect how much weight we put into the idea of "performance".

Perhaps the following comparison feels like a stretch, but IEEE-754 looked attractive performance-wise in 1985, too. And now we're stuck with uncountable Medium articles explaining why 0.1 + 0.2 is not 0.3 in languages that implemented the standard.

Since we have a lot of prior-art for pipes across multiple languages, do we have a rough idea of what we may be inflicting on our future-selves if we too-heavily consider performance issues that they won't necessarily be constrained by? Notice I'm not saying that performance isn't important, but rather how much it should factor into language-features that, once implemented, are pretty much permanent.

@kawazoe
Copy link

kawazoe commented Sep 18, 2021

You're comparing apples to oranges. Those are two different add functions.

Yes, they are, and F# encourages the add(1) style, which will likely need to allocate intermediate closures. This is the performance concern with F# pipes.

I see your point now. I would assume this is the same in RxJs, right? Their pipe function expects you to use operators that always return a new function. For instance map(x => x + 2) will return a function that takes an observable as its single argument. I guess it is less of an issue here since you won't create a ton of observables in a hot path in the first place.

The thing is, what you said was a concern here is exactly what the FP community wants in a pipeline operator. It is present in pretty much every single implementation I can think of. This is something that needs to be discussed elsewhere because it is definitely off topic here, but while there might be performance concerns with currying, there is also many code quality advantages that a more traditional pipe form like F# would encourage. One of them (I'll link the topic when I have it, for now ) is about pipeline state extraction which is a common problem right now in RxJs projects. While Hack style doesn't do anything different than the status quo about it, the F# proposal actively discourage this behavior. This is only possible thanks to it accepting a function instead of an expression.

In other words, the performance issue caused by the traditional form of a pipeline operator is in direct opposition to its main advantage: improving code quality. While the advantages are (I think?) already proven through other languages, the performance issue is (appears to be) 1/ avoidable through compiler optimizations in some cases and developer optimizations in the others, and 2/ while totally real in a micro-benchmark scenario, remains as speculation in real life projects.

I think this illustrates the frustration of people around this issue really well. We're debating code quality vs performance, and we have to blindly trust compiler experts that performance is more important, even though it appears now that there might be some data contradicting their claim? Not only does this goes against everything that I have learned as a developer, mainly to stay empirical, and especially about performance optimizations, as well as prefer higher level constructs until performance becomes an issue, but it also goes against everything I have learned as a human being. I think the Current Times shows it but too well. Trusting hard data for important decisions is how you move forward.

This was my initial point. I'm not here to fight for Hack or F# or minimal*PFA. I only care that the best proposal gets in the language. I accept that best is often hard to define, but I refuse the premise that it can be done through speculation and expertise only. This is why I asked if there was any data backing these claims. You said that this was a phase 3 step, but how can you compare performance between solutions if only one remains? It's simply illogical. Anyway, now I know there isn't any data, and that answers my initial question.

@ljharb
Copy link
Member

ljharb commented Sep 18, 2021

I don't see it as performance being more important (i often claim that performance is the least important consideration), but that the browsers often will refuse to implement things that hurt performance or that seem unoptimizable, even in the face of code quality arguments.

Given that every browser engine is open source, anyone who thinks that F# pipes can be heavily optimized could certainly make a branch that implements them, and potentially demonstrate that it's a viable path forward. Short of doing that, though, literally the only data available around performance and optimizability is "engine implementors' good-faith opinions", and that's what we typically rely on.

@mAAdhaTTah
Copy link
Collaborator

When performance concerns are raised, are yearly advances in hardware/compilers also taken into consideration?

Not really. The performance differences between the two aren't going to change because hardware gets faster. It's not possible for hardware to overcome the fact that one implementation requires the allocation of a closure that the other doesn't.

Since we have a lot of prior-art for pipes across multiple languages, do we have a rough idea of what we may be inflicting on our future-selves if we too-heavily consider performance issues that they won't necessarily be constrained by?

It's worth noting that other languages with |> operator have other characteristics which mitigate or eliminate the pain inflicted by point-free programming. F# & Haskell are both autocurried, compiled, and statically typed, all of which provide a compiler a significant amount of ahead-of-time information that can be used to optimize the program around these constructs. Elixir is dynamically typed but inserts the pipeline value in as the first argument, which obviates the requirement to allocate a new closure. Hack the language requires a placeholder, so the compiler can simply evaluate the expression. If JavaScript adopted an F# pipe, it would be, as far as I'm aware, the first language that would require the VM to allocate a new closure to evaluate the RHS of the pipe in in the point-free case.

What's more, the pipe function as implemented by most JS FP libraries avoid this concern by being composition functions, e.g. pipe(a(1), b(2), c(3)) returns a new function, which can then be called multiple times without allocating new closures (because they were allocated up front at the point the new function is created). While x => x |> a(1) |> b(2) |> c(3) looks functionally similar, the closures need to be allocated every time that function is invoked. Obviously, this is avoidable if you extract a(1) or wrap it |> x => a(1, x) but at that point, you've lost all of the advantages of F# over Hack.

I'm walking through all of this not because I care deeply about performance. As I mentioned, it's reasonable question to ask how common the hot paths that would result in this problem really are. Instead, I emphasize this because performance is not the only place where features found in those languages aren't necessarily well-suited to JavaScript because of how they interact with other features of the language they're a part of.

@js-choi js-choi added documentation Improvements or additions to documentation question Further information is requested labels Sep 19, 2021
@ken-okabe
Copy link

ken-okabe commented Sep 19, 2021

@ljharb

I don't see it as performance being more important (i often claim that performance is the least important consideration)

I agree, in fact, I was very surprised to see this matter is considered as if in very high priority.

If we prefer the machine performance over the abstraction of programming language, I think they should use assembly language.

In our history of software development, we have chosen to make the code more abstract than primitive machine code.
Actually, there is a history of functional language itself that has been avoided because of the performance issue including FP-Haskell language.

Abstraction is a way to write robust code, and which helps the software development much more productive because bug-fix is the major obstacle to productivity.

Denying and blocking the abstraction due to the machine performance issue or browser implement, that directly corresponds to denying and blocking the productivity of software development.

Is this what they mean? Well, let's vote among the entire software development community. I guess not.

@kawazoe

I think this illustrates the frustration of people around this issue really well. We're debating code quality vs performance, and we have to blindly trust compiler experts that performance is more important, even though it appears now that there might be some data contradicting their claim?

I wonder how much is the overhead, I want to hear how many percent overall.
1%? or 15%

If the decision has been made due to performance over the code quality, I think the decision-makers should announce this fact. I'm pretty sure the JS community is very frustrated (on-topic) because their priority must be the code quality and bug-free code and will demand concrete evidence.

@voronoipotato
Copy link

voronoipotato commented Sep 20, 2021

I wanted to say thank you @js-choi for acknowledging our concerns, regardless of whether in the end you're able to accommodate them. I appreciate it.

It would be awesome to have a tacit pipe akin to what we have in many frameworks (rx, ramda, underscore.js ). Fable the F# to javascript compiler has working pipes and all it does is wrap the functions x |> f |> g -> g(f(x)) and it's used in production code and without any meaningful performance impact. https://fable.io/repl/ Even if that was all we got, I think we could work with that. I'm perfectly happy with writing unary versions of functions, assigning them to nice descriptive variables, and piping them through.

Example 1

var addThree = a => a + 3 
var wiggleNumber = a => doSomeThings(12,10,a,84,"three")
var x = 10
var y = x |> addThree  |> wiggleNumber |> (a => x + a )
// equivalent to var x = 10;  (a => x + wiggleNumber(addThree(a))(x)

Already in this case I have a nice clean line with clear descriptors, no new syntax (Other than |>), predictable and easy to understand and I can effortlessly nest it if the need arises. I don't need to teach anyone the meaning of an expression scope in a placeholder and how that is similar to or different from anything else. There's no new symbol that I need to talk about. There's nothing new I need to learn. In the nested example I have a "step" where I add the original to the current value. The example is contrived, but hopefully that helps illustrate the value of being able to describe steps and substeps with just a lambda. Whether or not its possible to write this in placeholder style is beside the point, this is what most of us are used to and have grown to trust. I would gladly put off automatic currying so long as it meant I get real pipes today. People insisting that this style isn't valuable without currying are missing the heart of what we're trying to convey. Placeholder pipes have a high cognitive overhead because they're an entirely new construct, with new scope, new edgecases, and even when we learn that, we'll have to teach it to others. Function pipes by contrast leverage existing language features, and so are easy to develop an intuition around.

In this regard placeholder pipes don't suit my needs. If they provide a new scope akin to a lambda that's extremely unintuitive (why not just use a lambda?), and if they don't then I would just reassign a variable as shown below (ex:2) rather than trying to figure out how a new concept I'm not familiar with works in an already quite complex language. I think placeholder pipes may an impression of writing in functional style that will surprise and potentially confuse people who are expecting functional pipes, as the |> is more commonly used to mean a function pipe. For example, I had seen people excitedly bring up |> in the context of javascript several times, and not once did I see anyone independently of this working group bring up the concept of a placeholder. It may be a novel solution, but I think it's too novel of a solution that is going to leave people feeling confused.

Example 2

If it doesn't have a function scope why wouldn't we just do this?

var x = 10
x = x + 3
x = doSomeThings(12,10,x,84,"three)
x = x + 10

@voronoipotato
Copy link

voronoipotato commented Sep 20, 2021

If it helps understand how Fable (F# Babel) did it, for currying Fable just de-sugars to a new lambda when you don't provide all the arguments. Similarly pipes are literally just g(f(x)), I've tried to provide some examples with more than one argument.

//F# Code
let a x w = x * 3 + w
let b x = x / 7
let y x = x |> a 7 |> b
let z = a 8
//JS Code (translated from F# using fable) 
function a(x, w) {
    return (x * 3) + w;
}

function b(x) {
    return x / 7;
}

function y(x) {
    return b(a(7, x));
}

const z = (w) => a(8, w);

Fable is fast, and I've never ever felt the need to limit the usage of lambdas in either javascript or F#.

@arendjr
Copy link

arendjr commented Sep 20, 2021

@js-choi wrote:

I think Hack pipes (which address many of their concerns) actually would help everyone, too (#217).

As someone who is not part of the JS FP community (I’ve never even tried using RxJS or Ramda, because I think there’s too much friction between the FP communities and the rest of the JS ecosystem, though I have played around with ReasonML and do enjoy the influx of functional features in modern languages), I want to say I do not share this sentiment for reasons outlined in #225. I would rather have the language remain without any pipe operator than to have to deal with Hack in the future.

I too would like to thank @js-choi and the other contributors for all their effort, but I believe the current direction to be misguided to the detriment of not just the FP community, but the JS community at large.

@micahscopes
Copy link

micahscopes commented Sep 24, 2021

Thank you for articulating this, it puts a finger on my disappointment.

I've been working on an application using most.js and F# pipeline operators via babel, and had really been looking forward to this proposal advancing. I was pretty surprised that Hack pipes got picked, as I hadn't paid much attention to them.

(As a reminder, the minimal F# proposal is the default option in babel-plugin-proposal-pipeline-operator, which is definitely contributing to my surprise.) edit: it's not the default after all

Tacit syntax is the reason I was looking forward to the pipeline operator. Naively I thought that was the whole point of this proposal. While I appreciate the explicitness of the hack syntax in the case of applying the pipeline operator to functions with multiple arguments, it's hard work to reason about nested streams, and the tacit style is something I've come to lean on cognitively.

Consider some real life code I use to repeat the beginning of a stream called notes$ as a higher order streams, a function called repeat. Here's how I use it to repeat 4 bars of some musical notes and play them to a synth:

midiClip('melody-A') |> repeat(4*BAR) |> playNotes('synth-preset-z')

And how the function is written with the minimal F# pipeline operator:

const repeat = (duration) => (notes$) =>
  periodic(duration)
  |> map(() => notes$ |> until(at(duration)));

This is already pushing it for me, but I can fairly quickly see what's going on, and if I need to debug or replace the inner stream, it's not too much work to figure out the parentheses.

Here it is with Hack pipes:

const repeat = (duration) => (notes$) =>
  periodic(duration)
  |> map(() => notes$ |> until(at(duration))(^))(^);

So now I imagine, what if I'd made a mistake on the inner stream? In addition to having to think about the stream logic, now I have to think about how to correctly rewrite until(at(duration))(^))(^) as I try something different. But then again, maybe that was the source of my mistake in the first place? Maybe the stream logic is fine, it's just that I wrote until(at(duration)(^)))(^) instead of until(at(duration))(^))(^) or even until(at(duration)))(^))(^) for that matter?

For the record, here it is with a pipe function:

const repeat = (duration) => (notes$) =>
  pipe(
    periodic(duration),
    map(() => pipe(notes$, until(at(duration)))),
  )

I guess that's not too bad, but I'll miss some things:

  • the quickness of just throwing in |> when I want to pipe something inline, without having to worry about additional parentheses
  • being able to glance at the immediate area around an expression or identifier and know right away if it's part of a pipeline, without having to search both back for the last pipe and ahead for its closing parenthesis
  • being able to quickly scan for |> and see the extent of a pipeline

Anyway, thanks for acknowledging this frustration!

@lightmare
Copy link

lightmare commented Sep 24, 2021

Here it is with Hack pipes:

const repeat = (duration) => (notes$) =>
  periodic(duration)
  |> map(() => notes$ |> until(at(duration))(^)))(^);

Yes, that's horrendous. If you were to use Hack pipes in this code, you'd need an appropriate set of functors to make that somewhat readable:

const repeat = (duration) => (notes$) =>
  periodic(duration)
  |> h_map(^, () => notes$ |> h_until(^, at(duration)));

@kawazoe
Copy link

kawazoe commented Sep 24, 2021

I've been working on an application using most.js and F# pipeline operators via babel, and had really been looking forward to this proposal advancing. I was pretty surprised that Hack pipes got picked, as I hadn't paid much attention to them.

@micahscopes I'd like to point out that it is also the case in TypeScript. There are currently two PRs that covers both the F# and the Hack proposals ( microsoft/TypeScript#38305 vs microsoft/TypeScript#43617 ) and the F# proposal got an order of magnitude more traction than Hack-style. It's nice to see them both covered, but it does feel like what people wanted out of this was tacit syntax more than a pipeline operator.

@micahscopes
Copy link

micahscopes commented Sep 24, 2021

So I did some more thinking and realized that the functions in use in my previous example are curried, so in the Hack style that repeat function could be simplified to:

const repeat = (duration) => (notes$) =>
  periodic(duration)
  |> map(() => notes$ |> until(at(duration), ^), ^);

I still find that to be pretty taxing though, having to worry about both |> and ^).

And after reading more of these comments, I'm realizing that it's not even the F# vs. Hack proposal conversation that I care about, it's the tacit syntax. I could likely be convinced that I prefer the Hack proposal as long as there was a "bare style" for composing unary functions.

I wonder how much of this agonizing drama about "Hack proposal vs. F# proposal" really just boils down frustrations over the burden on tacit syntax? Probably most of it, I'm guessing. As someone who wasn't following too closely until now, I just assumed this whole proposal was about making it easier to create and use these tacit style APIs, and to make these APIs more composable and extensible.

Thinking back, there are so many times when a JavaScript library with an expressive, declarative, tacit style API (D3, JQuery, etc.) has come around and I've wanted to extend it, only to realize I'd have to learn that library's own unique way of implementing its special, quirky point free API, which was a lot of overhead.

Working with the F# proposal & ES modules has been so freeing and enjoyable, and felt like a resolution to that old tension. I'm able to mix and match functions from lodash-es, tonal.js, most.js, fp-ts and whatever other misc. libraries I find in a way that feels very fluid and has been really expressive and productive for me. I've also found myself writing lots of nice little compact helper functions for use in pipelines that turn out to be really reusable.

I get that I'll be able to do that in Hack style pipelines, but I'll probably find myself going with pipe instead, for readability reasons, in spite of the ways I mentioned that I feel like it's lacking.

Something that sits uneasy with me is this whole idea of "not wanting to encourage tacit syntax"... but it's like, people are already doing it, and doing it in very clunky, burdensome ways. People go way out of their way to create these "fluent APIs", and libraries written with them are expressive and popular.

And there are ways to optimize. That burden isn't solely on the browser engine developers, it's on JavaScript developers too! The solution to that problem is education on how to cut down on anonymous closures, in my opinion, not discouraging people from writing APIs of a certain style by imposing an extra 3 characters for every line of their pipeline composition.

@runarberg
Copy link

runarberg commented Sep 24, 2021

Thinking back, there are so many times when a JavaScript library with an expressive, declarative, tacit style API (D3, JQuery, etc.) has come around and I've wanted to extend it, only to realize I'd have to learn that library's own unique way of implementing its special, quirky point free API, which was a lot of overhead. — @micahscopes

Indeed. And this is what worries me (or rather frustrates me) about the Bind -> operator proposal and the extensions :: operator proposal. Is that you have to work with this if you are extending a construct. Whatever this is is tightly coupled to the library implementation. The simplicity of tacit pipe operator |> allows you to simply create a function that takes in the construct and return a new value (potentially of the same construct if you are defining a new operator). And the beauty is that you don’t need to think about the implementation of the construct, or what this is as you operate on it. This works the same even if you are extending iterators where this is very uncertain and weird.

I’m afraid that the loss of tacit pipeline operator won’t be fully remedied with other proposals which only work on the prototype of what you are operating on.

@ghost
Copy link

ghost commented Sep 26, 2021

@mAAdhaTTah

I do not understand in what way the pipeline operator "encourages tacit programming'' any more than Array#map does. Some people choose tacit style, most choose lambdas; the trade-off is the same. I don't see why it would be any different for the pipeline operator. Most people will use lambdas and I see nothing discouraging them from doing so that doesn't already exist with Array#map and Promise#then.

@mAAdhaTTah
Copy link
Collaborator

People aren't designing whole libraries in curried / point-free style so they can use them with .map.

@ghost
Copy link

ghost commented Sep 26, 2021

In what way does that encourage a JavaScript user to go point-free? As I said, the trade-off seems to me the same as with .map. In fact popular FP libraries like rxjs, fp-ts and ramda do use curried functions inside their own map functions. The reason why they provide functions as an alternative to class methods is not because curried functions work better inside the former.

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Sep 26, 2021

The reason why they provide functions as an alternative to class methods is not because curried functions work better inside the former.

Correct, they provide them because curried functions work better with pipe, and we've got whole threads of people who want pipe the function to be directly translated into |> the syntax so they can use their curried functions. If the F# pipe didn't encourage point-free programming, we could just require a placeholder and no one would be upset. The outrage itself is a counterargument.

@ghost
Copy link

ghost commented Sep 26, 2021

Enabling is not the same as encouraging. People switching over from pipe to |> does not add any performance burden to the web unless anyone actually feels encouraged to start using point-free programming. As I said, they already are free to do so inside HOF callbacks (just like FP library users do), and they don't. They use lambdas. And they would also use lambdas with a functional pipeline operator.

@lightmare
Copy link

If the F# pipe didn't encourage point-free programming, we could just require a placeholder and no one would be upset. The outrage itself is a counterargument.

It seems the outrage is coming mostly from circles that already do point-free programming. The rest of the world probably has a reason they don't, and I doubt the lack of pipe operator is the main reason.

The concern that F# pipe would on its own increase proliferation of point-free programming, is hard to believe.

@tabatkins
Copy link
Collaborator

The concern that F# pipe would on its own increase proliferation of point-free programming, is hard to believe.

If it doesn't, that implies either that pipeline is adopted solely within the existing pipe()-using population (and thus likely not worth adding to JS as new syntax, as it would be only a very tiny improvement in usability with no new functionality), or greater adoption occurs but is done solely via syntax that isn't point-free (mostly arrow funcs), which has the exact same usage patterns as Hack-style but with additional syntax weight and special-cases (and thus isn't worth adding either).

F#-pipe heavily encourages point-free programming, and I don't think it's very reasonable to deny that. That pipeline style is popular within auto-curried languages for a reason, since "point-free" is very cheap and easy there.

@lightmare
Copy link

If it doesn't, that implies either ... or ...

Could also be both simultaneously. I agree that F# pipe has more syntax issues; I just don't buy that 1. point-free is inherently bad, and 2. people will switch paradigms solely because a new operator allows. There needs to be actual perceived benefit for such a switch to happen.

@micahscopes
Copy link

micahscopes commented Sep 26, 2021

@tabatkins What were the concerns with enabling more graceful tacit programming in JavaScript outside of the memory related concern over potential for excess closure allocations? I'm hearing something about risks of ecosystem bifurcation, but would make the case that this is already happening in part because of the longstanding difficulty implementing extensible fluent style / tacit APIs in JavaScript.

Have you ever tried writing a D3 plugin? It stinks ☹️. Yet D3's API, as tricky as it might be to get used to, is amazingly powerful and expressive. A lot of popular libraries have these "fluent APIs" that are hard to get to interoperate or extend.

Tacit programming shouldn't be used everywhere, but it's an awesome way to implement declarative APIs, and a lot of people really want that and will go out of their way to get it regardless of how the pipeline operator turns out.

After reading a lot of these posts, I've come to the conclusion that I'd be just as satisfied, if not more satisfied, with just a (preferably left to right) function composition operator, but even if that were to alleviate concerns about making it too easy to allocate lots of closures, it seems like there's a serious opposition to tacit programming regardless of those performance concerns, so maybe a function composition operator would face an uphill battle too?

@tabatkins
Copy link
Collaborator

this is already happening in part because of the longstanding difficulty implementing extensible fluent style / tacit APIs in JavaScript.

This is a legit complaint; the only methods in today's JS for doing fluent-style APIs are a bunch of methods on the prototype of an uber-object (not tree-shakeable, not easily extensible, requires uber-object which isn't compatible with all library patterns) or pipe() + point-free functions.

But there are many ways to enable fluent-style APIs, and the current pipeline proposal does that just as well as F#-style syntax would. The current proposal requires you to explicitly pass the context object (which in fairly idiomatic JS would be the first argument), but in return you get a function that can be called with or without a pipeline and feels natural either way - recolorGraph(nodes, {stuff}) for one-offs, or nodes |> recolorGraph(^, {stuff}) |> moreD3(^, "foo", {moreStuff}) for chained operations - and is written in a familiar style that meshes with the large majority of functions that are written for JS.

There are some more specific reasons why I, personally, am not a fan of promoting point-free programming in JS (despite a substantial casual history in HOF languages), but they're not relevant to the fluent-style question.

@shuckster
Copy link

There are some more specific reasons why I, personally, am not a fan of promoting point-free programming in JS

Sure would be nice to know what those are my good fellow. Might we please inquire?

@tabatkins
Copy link
Collaborator

In short, heavy use of point-free programming produces code that is difficult to read and understand. I can explain how doubleFmap = fmap fmap fmap works in Haskell, but doubleFmap fn = fmap (fmap fn) is imo substantially clearer and easier to understand; the moment flip comes out, I'm rewriting your function instead. As @js-choi has linked to a couple of times, the F# documentation provides a pretty detailed explanation of this, which I completely agree with.

@js-choi
Copy link
Collaborator Author

js-choi commented Sep 28, 2021

This is a friendly reminder to everyone: Although this issue is for discussing people’s understandable frustrations about this proposal’s process—as well as their feeling of loss about something that was felt promised—it also still falls under the TC39 Code of Conduct. Please try to follow the spirit of the CoC, including being respectful, friendly and patient, careful in the words you choose, and constructive. Thank you! : )

@icopp
Copy link

icopp commented Oct 17, 2021

As someone who wasn't following too closely until now, I just assumed this whole proposal was about making it easier to create and use these tacit style APIs, and to make these APIs more composable and extensible.

This is the ongoing reaction I've had to all the business around the pipeline operator. At first, as someone who regularly uses Ramda but avoids the cases where function nesting or pipe() become hard to read, I thought that was the entire point: "wow, this will make data handling stuff with function composition so much easier". Then obligatory placeholders and the like kept popping up and I started wondering what the point even was, given that they add back in the extra mental overhead from removing pipe() wrappers.

@andykais
Copy link

There is one more benefit that I always consider first in regards to this proposal, which is a tack-on effect of adding F# and PFA. I believe typescript would be forced to include a native mechanism for typing piped functions if this proposal landed as F# microsoft/TypeScript#30370. Imo using a pipe method vs some new syntax doesn't make a ton of difference, but it would likely have the typescript team reconsider the priority of better functional type support. We could all avoid writing overloads like these https://github.com/ReactiveX/rxjs/blob/938e2118ae2762d27ba270b26bff76e674a0b799/src/internal/Observable.ts#L349-L415.

I am aware of that its backwards to look forward to a typescript change because of a spec change but these are my opinions and hopes

@tabatkins
Copy link
Collaborator

I don't think it's very valid to make JS language choices for the purpose of pressuring TS into particular prioritization choices, yeah. ^_^

@kawazoe
Copy link

kawazoe commented Oct 25, 2021

@andykais This really is a TypeScript problem, and it has been fixed with variadic types. You can now do something like this: https://github.com/malinges/ts4-variadic-compose/blob/main/compose.ts

@andykais
Copy link

@kawazoe that code seems busted, even on 4.5 beta typescript

@kawazoe
Copy link

kawazoe commented Oct 27, 2021

@andykais I've used this snippet in TS 4.1 when they introduced the feature and it worked fine. It probably needs some fixes for the most recent versions though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation question Further information is requested
Projects
None yet
Development

No branches or pull requests