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

Hack is dead. Long live F#. #205

Closed
14 tasks
kiprasmel opened this issue Sep 13, 2021 · 199 comments
Closed
14 tasks

Hack is dead. Long live F#. #205

kiprasmel opened this issue Sep 13, 2021 · 199 comments

Comments

@kiprasmel
Copy link

I would like to thoroughly discuss the pros & cons of the F# vs Hack & other proposals.

I will have questions at the end that I would like answered by the committee & others involved.


I want to argue that the method for choosing the proposal (F# vs Hack) is wrong.

In @tabatkins discussion on the differences of the proposals, their conclusion on the differences is this:

Ultimately... very little [differences]. <...> A three-character tax on RHSes in various situations.

I disagree. There are big differences between the proposals, and they cannot be measured mearily by the amount of characters. Let's dive in.

1. Function composition

I'll start from the async/await & yield argument against F#:

With Hack, you can do this:

fetch("/api/v1/user")
 |> await ^.json()
 |> await ^.name
 |> console.log(^)

with F#:

fetch("/api/v1/user")
 |> await
 |> res => res.json()
 |> await
 |> user => user.name
 |> console.log

with F#, you must have the await keyword on a new line/pipe, meanwhile with Hack you can have it at the same line.

I argue that F# is better than Hack because in the Hack example, you are doing 2 things at once: taking the res.json (which is further hidden by the ^ or whatever token), AND ALSO awaiting the promise. It's a bigger cognitive load, as opposed to the F#'s way where only 1 thing is done per 1 line/pipe.

As mentioned by @runarberg, the superiority of Hack over F# regarding async/await is very questionable - the claims are not backed up in the current proposal's README and further documents, and as a given example, the Rust's community has, after a very thorough RFC, went with the same exact solution that F#, not Hack, would allow.

Discussed further in #204


Another point by @OliverJAsh is that for Promises, they are already "pipe-able" via .then, and that it is completely possible to live without the async/await support in pipeline operators:

fetch("/api/v1/user")
 .then(res => res.json())
 .then(user => user.name)
 .then(console.log)

And going even further, you could say we don't need pipeline operators at all, because they're already here (the F# way):

["name"]
 .map((name) => name.toUpperCase())
 .map((name) => "hello, " + name)
 .map((name) => name + "!")
 .map((name) => console.log(name));
// hello, NAME!

Promise.resolve("name")
 .then((name) => name.toUpperCase())
 .then((name) => "hello, " + name)
 .then((name) => name + "!")
 .then(console.log);
// hello, NAME!

// F#:
"name"
 |> (name) => name.toUpperCase()
 |> (name) => "hello, " + name
 |> (name) => name + "!"
 |> console.log
// hello, NAME!

this is possible, because both [].map and Promise.then are, in FP terminology, Functors. See a quick explainer. In essence, they preserve function composition, e.g. g(f(x)) is the same as x |> f |> g, which is fundamentally what the pipeline operators are about!

Heck, you could create your own Functor:

function Box(x) {
	return {
		map: (f) => Box(f(x))
	}
}

Box("name")
 .map(name => name.toUpperCase())
 .map(name => "hello, " + name)
 .map(name => name + "!")
 .map(console.log)
// hello, NAME!

and get the same effect as F# proposal, though without special cases for async/await & yield.

So then, why do we even need the pipeline operator?

In F#'s case:

  • it's more accessible (no need to create your own Box)
  • same benefits as the Box - reduces complexity of g(f(x)) into x |> f |> g
  • special cases for async/await & yield would be a great addition
  • combination with the Partial Application proposal improves F#'s pipe and the whole language even further, making it the best option, beating out Hack (proposal 2), and even Hack with the |>> operator (proposal 3), without (!) the need for an extra operator (we'll get to that).

Hack would provide the same benefits as F#, but worse - let's dive in further.

2. The Downfalls of Hack

What the Hack proposal offers is the ability to reference the current variable with a special token (e.g. ^), instead of creating an arrow function:

"name"
 |> ^.toUpperCase()
 |> "hello, " + ^
 |> ^ + "!"
 |> console.log(^)
// hello, NAME!

which maybe looks good first, but not when you consider further:

say we have utility functions:

const toUpper = (x) => x.toUpperCase();
const greet = (x) => "hello, " + x;
const sayLoudly = (x) => x + "!";

with F#, you can just do this:

"name"
 |> toUpper
 |> greet
 |> sayLoudly
 |> console.log
// hello, NAME!

meanwhile with Hack, you'd have to call the function each time:

"name"
 |> toUpper(^)
 |> greet(^)
 |> sayLoudly(^)
 |> console.log(^)
// hello, NAME!

is this a big deal? Yes.

Because you can replicate Hack's behavior with F#, but cannot replicate F#'s with Hack:

// F#, replicating Hack:

"name"
 |> (x) => toUpper(x)
 |> (x) => greet(x)
 |> (x) => sayLoudly(x)
 |> (x) => console.log(x)
// hello, NAME!

whereas you just cannot have function composition with Hack, without calling the functions with the token

// Hack. This will NOT work:

"name"
 |> toUpper
 |> greet
 |> sayLoudly
 |> console.log
// Error

meaning, F# = freedom of choice, and Hack = no freedom of choice (in 2 cases - 1st, composing curried/unary functions concisely, and 2nd - choosing the name of the argument, as opposed to a predefined symbol(s)).

With F#, instead of Hack, an added benefit is that:

  1. you do not need a custom token (e.g. ^) to make it work. it's just a function with an argument!
  2. you can have both behaviors and choose the appropriate one yourself, instead of being forced into using using ^.
  3. you can specify the variable name yourself by creating an arrow function, which is more readable than any token the committee would end up going with anyway.
  4. it is easier to copy-paste/extract code into it's own function, whereas with Hack you cannot do this, since the current value token ^ is only usable in the scope of pipeline operators
  5. combined with the Partial Application proposal, it can be used just like Hack, and even better.

3. The Partial Application proposal

What if your function takes in multiple arguments, but with the F# pipeline operator you can implicitly provide just one argument (otherwise you need to create an arrow function)?

This is what Hack oughts to solve:

const sayLoudly = (x, howLoudly) => x + "!".repeat(howLoudly)

// F#
"name"
 |> toUpper
 |> greet
 |> (x) => sayLoudly(x, 5)
// hello, NAME!!!!!

// Hack
"name"
 |> toUpper(^)
 |> greet(^)
 |> sayLoudly(^, 5)
// hello, NAME!!!!!

great. maybe. the third pipe is supposedly better in Hack than F#, but F# is still better in the first 2 pipes.

But what if? What if F# could get even better?

Here comes Partial (Function) Application, PFA for short:

// same as before:
const sayLoudly = (x, howLoudly) => x + "!".repeat(howLoudly);

// F#
"name"
 |> toUpper
 |> greet
 |> sayLoudly(?, 5)
// hello, NAME!!!!!

// desugars to:
"name"
 |> toUpper
 |> greet
 |> (temp1) => sayLoudly(temp1, 5)
// hello, NAME!!!!!

hooray!

What happened here?

The sayLoudly function got curried / turned into a "unary" function, meaning that the argument x is now the only argument that the function takes in, and all others are inlined.

As you might notice, this looks exactly the same as Hack, just a different token (? instead of ^)

But there's more!

Since this is Partial Application, it is not specific to pipeline operators, as opposed to the Hack's pipeline operator with a token that's only available within the pipeline.

Meaning, you can use partial application anywhere in the language!

Promise.resolve("name")
 .then(toUpper)
 .then(greet)
 .then(sayLoudly(?, 5))

["name"]
 .map(toUpper)
 .map(greet)
 .map(sayLoudly(?, 5))

Box("name")
 .map(toUpper)
 .map(greet)
 .map(sayLoudly(?, 5))

This is the way.

This is exactly what I am advocating for.

And I'm not alone:

4. The JS Community

  • @benlesh, RxJS creator, highly knowledgeable in the FP space:

( https://gist.github.com/tabatkins/1261b108b9e6cdab5ad5df4b8021bcb5#gistcomment-3885521 )

To get what we want with Hack Pipeline without hamstringing the entire JavaScript functional programming community, the ideal solution (that would please everyone) is to land the F# pipeline, and then focus on the partial application proposal.

and further problems with the Hack proposal: ReactiveX/rxjs#6582

Currently our [RxJS] "pipeable operators" rely on higher-order functions. Using these with the Hack Pipeline will be cumbersome, ugly and hard to read: <...>

( #203 (comment) )

A nagging sensation came to me as I looked at the README: pipe() examples are conspicuous by their absence:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x)

pipe(
  x => one(x),
  x => two(x),
  x => three(x),
)(value)

Why are there no example like this? It's a common and unremarkable pattern in FP-JS circles. Even without the exciting point-free potential, surely F# would be synchronous with this? Is there a use-case that Hack solves better that justifies its idiomatic deviation?

This is very similar to my Box Functor example above, and goes hand-in-hand with what @benlesh says, highlighting yet again why F# > Hack.

( https://gist.github.com/tabatkins/1261b108b9e6cdab5ad5df4b8021bcb5#gistcomment-3757152 )

the only downside of F# style is you have to write 3 extra characters per pipe.

Exactly, and as discussed, this is not even an issue, and definitely not enough to justify Hack > F#.

This combined with the cluster that is the discussion about what should be used at the placeholder [token] further pushes me in favor of F# style.

Exactly what got me into this too - the discussion of what special token to use for Hack (I've used ^ here) is a good indicator that the Hack proposal is lacking. People were considering tokens such as ^, %, $_, $$, and other ridiculous ones, because the initial ones (# / $) are incompatible with js -- all of this mess to avoid a simple arrow function with F#. The most upvoted comment there also reaches the same conclusion.

  • @rbuckton, TC39 member, Champion of the Partial Application himself:

( #91 (comment) )

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.

As I've argued here, I think it's the opposite - it's harder to argue against F#'s pipes rather than Hack's, especially if combined with Partial Application.

If we kept debating on F# vs. Hack, neither proposal would advance, so my position as Co-champion is "tentative agreement" with Hack-style.

Here I strongly disagree. A half-baked solution (Hack, especially without the |>> operator) that will be impossible to revert once it's shipped is FAR worse than no solution at all. And then we have an a far better (AFAIK?) solution: F# + Partial Application, which seems unbelievable that it is not the one that's being considered and advancing into further stages.

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 [Partial Application] works well for data-first libraries like underscore/lodash.

I agree, and so does @benlesh & others.

However, F#-style is harder to use with yield, await,

Disagree. See 1. Function composition.

TL;DR: If considered thoroughly - async/await is actually an advantage of F#, not a disadvantage, with a proven track record by the Rust community.

and methods where the topic is the receiver (i.e., ^.method()), which is an advantage for Hack-style.

yes, but this is literally the only advantage of Hack, as discussed above. x => x.method() is just as viable, and works just as well in other cases where Hack is supposedly advantageous. As I already argued above, I think the F#'s way is actually better even in this case - see the last two paragraphs of 2. The Downfalls of Hack, or even better the whole essay. Overall, there's no way the "inconvenience" of doing this the F# way with an arrow function instead of with Hack outweights all the benefits of F# + Partial Application.

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.

This is regarding the 3rd proposal - Split mix - Hack's |> + F#'s |>>. I agree - it is not very tenable, because we would introduce yet another operator. Furthermore, why all this complexity, multiple operators etc., when we can have the best of both worlds with F# + PFA?

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.

Once again - disagree, because a half-baked solution is worse than no solution at all. Digging further, however much time it requires, is definitely worth it, considering how much impact it will have.

This is where I, and a big part of the JS community, needs people from the TC39 committee like @rbuckton, @RyanCavanaugh, @littledan, potentially @DanielRosenwasser & others in favor of the F# approach to stand up (others are just as welcome).

Just look at the interest, discussion, amount of upvotes in the TypeScript's repo for implementing:

a) the F# operator: microsoft/TypeScript#38305 (240+ upvotes, 0 downvotes)

b) the Hack operator: microsoft/TypeScript#43617 (12 upvotes, 8 downvotes)

Clearly, A is preferred over B. Of course, A has been out longer, but the general consensus is pretty clear!

As the implementor of the above 2 PRs, @Pokute, mentions in the @tabatkins discussion:

It's unfortunate that partial application proposal is not mentioned here, since it complements F# pipelines. F# pipelines practically has the burden that partial application as part of F# pipeline proposal is so easily to separate as a distinct proposal that no-one can really object to separating it. Hack-style #-placeholders can not be separated into a separate proposal. Thus I feel like it's unfair to do comparisons where F# pipelines don't additionally have the partial application to help them. It results in a weird situation where the robustness of partial application actually hurts F# pipeline argument.

Initially, the pipeline operator, back from 2018 or whenever, has always been shown first as the F# version, and has only been changed in the README recently to accomodate Hack moving into Stage 2. This is not what the community actually wants! When we talk in general about the Pipeline Operator and how much people want it (e.g. @littledan's slides), most if not all who are excited about it, are still refering to that same version they saw - the F# one.

There are

many @peey,
many @anatoliyarkhipov,
many @nmn,
many @nmn,
many @nmn,
many @samhh,
many @Avaq,
many @OliverJAsh,
many @OliverJAsh,
many @jleider,
many @OliverJAsh,
many @benlesh,
many @tam-carre,
many @benlesh,
many @benlesh,
many @lightmare,
many @kiprasmel,
many @stephaneraynaud,
many @samhh,
many @shuckster,
many @aadamsx, and definitely even more

arguments in favor of F# over Hack, or concerns with Hack, some of which have already mentioned in my writings above.

The few that are in favor of Hack over F#, are far and few in between. Surely, if more people liked Hack over F#, there would be more arguments from their side as well, or at least attempts to answer the F# over Hack arguments thoroughly, which I definitely feel has not been done well enough.

And while I somewhat agree with @tabatkins that:

Counting the comments on a gist is not a representative sample; there's an obvious bias towards comments that are arguing against the OP rather than agreeing with it.

, you simply cannot deny the fact that there indeed is a lot of people in favor of F#, and I am pretty certain it's greater than those of Hack.

Heck, even if the count of people doesn't matter, the arguments for and benefits of F# over Hack are definitely more prevalent and agreed upon than those of Hack over F#, at least in the JS community itself, but apparently not in the TC39 Committee.

5. A metaphor

For me, F# vs Hack is like React vs Angular.

I'm not looking to start a framework/library war, so hear me out:

Why React is great is because it's as close to the javascript language as it can be. Unlike Angular, it didn't try implementing literally every aspect of the language in it's own way with e.g. template rendering and all the mess that comes with it -- for-loops/conditionals as html attributes, 2-way data binding, etc. -- whom will never be better than javascript itself; classes + decorators + dependency injection; specific libraries that only work with Angular; etc. In React, you take existing javascript libraries and just use them, or create your own, who will also be usable elsewhere outside of React (with an exception for Hooks, but that's just fine). It's just javascript, with sprinkles of jsx on top, and unlike Angular, you're not subdued by the authors of the framework/library on what you can or how you can do something, but rather by the language itself, which is a far better bet.

The mistake that Angular made, imo, is trying to re-implement the javascript language itself into an opinionated framework. React just bet on javascript and took off. Try creating a component in Angular - they have a whole CLI tool ready for you just to generate some boilerplate for every new component. React? Lol, create a function, and here is your component. It's just a function. It's simple. It composes well. It's what we love about javascript.

How is this related to F# vs Hack?

Well, there's a lot of parallels!

Hack wants to create an extra token (e.g. ^, or %, or $_, or whatever bogus combination of symbol(s) get picked by a small group of people, effectively enforcing their choice for everyone, instead of letting the individual choose themselves), and that token also won't be usable outside the context of pipeline operators.

F#, on the other hand, works better with curried functions, has an advantage with await being simpler (and has a track record of Rust to back this up, as discussed above in 1. Function composition), and is also just as viable as whatever Hack tries to solve, at the cost of creating an arrow function, which is actually better, because freedom of choice for the name of the argument! Furthermore - it does not introduce an additional token, which makes it easier for the ecosystem to adapt, AND it stays more in-line with the language itself, because, just like in React, it's all about the function. See also 2. The Downfalls of Hack above.

Add Partial Application to F#, and you've got yourself exactly what React got with Hooks. PFA solves F#'s shortcomings and (arguably) beats the only remaining value proposition of Hack. Even better - Partial Application can be used outside F#'s pipeline operators, aka anywhere in the language!

F# is React, Hack is Angular.

Many people, including myself, have coded extensively in both - I can almost guarantee our opinions about the two are identical - React over Angular every. single. time.

6. Is Hack really dead?

I suppose there are limitations with the F# proposal as well? Sadly, I am not too aware of them (other than the things I've mentioned above which are actually not limitations but advantages), but more experienced people could give pointers? (Especially from different FP language backgrounds).

I probably shouldn't be the person writting this essay in the first place - there exist way more experienced people who could've done a better job than me. But I suppose someone is better than no-one.

There are some things I didn't cover, but this is already very long and should serve as a good starter / a continuation from the bikeshedding of Hack's token and the discussion of tradeoffs between F# vs Hack & others.

Thus, I invite everyone to discuss this Pipeline Operator thing further, and in no way rush into Stage 3.

In particular, I'd like at least the following concerns to be addressed, especially by members of the TC39 committee, especially those involved with pipeline operators, whichever side they're favoring (but everyone else is just as welcome too):

  • 1. What you just read here - does it make sense, what are the shortcomings you see, what else could be improved and addressed?

  • 2. Did you read anything new or was literally everything already considered?

  • 3. Do you have any plans to carefully study the usability of the proposals, just like @runarberg highlighted that the Rust community has done? If no - why, if yes - how, and will it have any impact before we reach Stage 3? Or have you already done this and where can we find the results?

  • 4. Do you have any concerns that the current proposal is incomplete / half-baked?

  • 5. Do you have any concerns that if we move forward with the Hack proposal, without including the F#'s |>> operator together at the same time, it could lead to us missing some detail which would prevent |>> (or whatever other token choice for this functionality) from being possible, while the Hack's pipeline operator would already have shipped, and it would be too late to go back?

  • 6. In general and in this case, do you think a half-baked solution is better than no solution at all -- even if there are ways to implement the solution already (Functors etc.), meaning the half-baked solution isn't necessary -- even after reading this and comments/arguments from other threads linked here -- even knowing the impact that it will have -- even knowing that we've made such mistakes in the past (nodejs callbacks vs promises)?

  • 7. Do you know any people in the TC39 Committee who have a strong background in functional programming languages, e.g. Haskell, F#, Elixir etc? Did they participate in the discussions regarding Pipeline Operators?

  • 8. Were there any experts in the FP field invited to participate/help make decisions with regards to the pipeline operator? Did they help? How did the committee handle their feedback, how much weight did it have?

  • 9. Would it be possible in the future to be more transparent of how exactly the decision was made?

  • 10. What members of the TC39 committee made the decision, what members were not present (e.g. @littledan because vacation? etc.)?

  • 11. Do you believe that certain people not being present could sway the decision in one direction or another, why, and how do you make sure both sides are represented fairly and equally?

  • 12. Is it still possible to switch from Hack's to F#'s proposal? Would you help facilitate the switch if you believed that F# is better over Hack, even if the switch could further delay the landing of pipeline operators, because in the long term it would be worth it?

  • 13. Would there be a way to collect all information in one place (or at least clearly link to additional resources from the main source of thruth which probably should be the proposal repo README / RFC issue), as opposed to the current situation with 1) the proposal repo itself, 2) @tabatkins gist (!), 3) discourse, 4) @littledan's slides, etc.

  • 14. What could I or any of the javascript community members do to further help with this? For this proposal, and in future ones?


Thank you. Long live JavaScript.

@thesoftwarephilosopher
Copy link

For point 2: actually hack can do more than f# can because you can do expressions like create arrays or use math operators etc. and a downside of f# is you have to create unary functions for all these things.

@kiprasmel
Copy link
Author

kiprasmel commented Sep 13, 2021

@sdegutis I have already considered such use cases in the above writing. Hack is not doing anything more, it's different syntax (which is very important), but it does nothing more advantageous than F# - quite the opposite. It's clear that I prefer a unary/curried function over whatever Hack has to offer, and I think I've explained it well enough. The fact that in such cases you need to use an (arrow) function is a pro, not a con. Read again.

@thesoftwarephilosopher
Copy link

I’ve read it and reread your ideas a few times. I still just don’t agree. You make some points but ignore other valid counterpoints. None of what you said so far is new to me and yet all the arguments are countered by proponents of Hack pipes to my satisfaction.

And you make a lot of objective assertions about how many people prefer F# over Hack pipes and assert a general community consensus which I think are jumping to conclusions without sufficient data. For example the typescript issue votes indicate nothing except one issue has had more attention than another.

The way you talk and reason here, I have only ever seen people use when they simply don’t understand the opposing viewpoint correctly.

@y-nk
Copy link

y-nk commented Sep 13, 2021

I'd rather not have await keyword being alone in a line. the "it's more cognitive load because it does 2 things at once" point depends on the person coding. dealing with non-superstar engineer on a daily basis i can tell they will be more confused by writing what you propose. You could definitely argue back "but they'll just have to get used to it, it's not much" but the cognitive load would be for them to make sense of something that intuitively wouldn't, which defeats your "react vs angular" comparison.

I'm taking on this particular argument because one of the things frontend people (like me) look forward to is to get rid of the promise chaining for something lighter in syntax and yet as simple to understand, and seeing that you're advocating we're good with .then() is kinda pointing out that your take would work for sync piping but not async ones, which is (imho) a set back in the recent evolutions of the language trying (hard) to reunite sync/async (praise async/await for the clarity it brought).

On couple of unrelated remarks:

Thanks a lot for the writing, i learnt a lot (and i really mean it). Links provided were all instructives and imho your argumentation is strong enough that you don't need to bring in famous names to make a point (it just feels you're pushing with a crowd of names where you don't have to, really).

Also, on your TS repo comparison you may also include (for fairness) that they're implemented 1 year appart and that F# was first (and so it's most likely that it would have the most upvotes).

Where you lost me in your demonstration is at comparing React/Angular only because you're clearly biased towards one. The comparison isn't fair since the 1st sentence of each paragraph is "Why React is great" and "The mistake that Angular made" which is not even slightly balance, and later concluded with "F# is React, Hack is Angular" to "nail the coffin". You should rather recognise that React and Angular are 2 frontend libraries not designed for the same purpose, don't cover the same scope, and their reach in functionality brings in design choices where the philosophy was different in implementation.

Also, you lost your cool here:

Hack wants to create an extra token (e.g. ^, or %, or $_, or whatever bogus combination of symbol(s) get picked by a small group of people, effectively enforcing their choice for everyone, instead of letting the individual choose themselves), and that token also won't be usable outside the context of pipeline operators.

...which i can understand but tbh that's not with that kind of writing that you'll convince the people against it (and deciding, nonetheless) to change their mind. But that's just my opinion, and i'm a nobody - feel free to ignore :)

PS: I, as well, don't like Angular but it's still not relevant comparison and you're not fair in it anyway :)

@nmn
Copy link

nmn commented Sep 13, 2021

As someone mentioned in the original essay I want to chime in with some thoughts.

I still think F# is better than the Hack-style proposal. My reasoning for this is not based on power, but because I prefer adding just an operator over new syntax.

F#-style proposal is adding just one new operator. JavaScript already has operators and arrow functions. Adding a single operator seems like it’s not bloating the language all that much. Perhaps people don’t fully understand higher-order-functions, but methods like .map are fairly commonly used in JS, and there are many developers that will always use an inline function instead of “point-free style”

Similarly, if the F#-style proposal progressed, I expect most developers to always use inline arrow functions to keep things explicit.

But, we already understand how function calls work and learning one new operator is simpler than learning a completely new bespoke syntax just for piping.

BUT, I don’t agree with stalling the HACK-style proposal if that is the proposal with the momentum. I’m honestly tired of waiting for this proposal and every time there seems to be a little bit of momentum an argument breaks out and progress gets stalled. I’m tired and will take the “worse” proposal if that’s what I can get.

It can also be argued that adding new bespoke syntax is how JS progresses at all. The class syntax is very bespoke and it chose to do things to look like other languages rather than try to stick to existing syntax where possible.

@kiprasmel
Copy link
Author

kiprasmel commented Sep 13, 2021

@y-nk hey, thanks for your input.

I'd rather not have await keyword being alone in a line. the "it's more cognitive load because it does 2 things at once" point depends on the person coding. dealing with non-superstar engineer on a daily basis i can tell they will be more confused by writing what you propose. You could definitely argue back "but they'll just have to get used to it, it's not much" but the cognitive load would be for them to make sense of something that intuitively wouldn't, which defeats your "react vs angular" comparison.

Here I have to disagree. The cognitive load discussion between you and me, and anyone else for that matter, are just personal preferences, but the Rust community has actually done the usability research and reached conclusions that I've already mentioned above.

Also, the fetch example I chose, is quite rough. Usually you'd have a utility, something like this:

const fetchJson = (url, opts) => fetch(url, opts).then(res => res.json());

fetchJson("/api/v1/user")
 |> await
 |> user => user.name
 |> console.log

which gets rid of the 2 additional lines of await and res => res.json().

Mind you that the fact that we had to do 2 consecutive awaits suggests that we should have a function on top of them, and only do it once, just like I did in the example above with fetchJson - fetch is a good candidate to be "wrapped".

See #91 (comment)

I'm taking on this particular argument because one of the things frontend people (like me) look forward to is to get rid of the promise chaining for something lighter in syntax and yet as simple to understand, and seeing that you're advocating we're good with .then() is kinda pointing out that your take would work for sync piping but not async ones, which is (imho) a set back in the recent evolutions of the language trying (hard) to reunite sync/async (praise async/await for the clarity it brought).

Async/await is great for day-to-day, but you should not underestimate the power of Promises and the foundational principles of FP that sits behind them, especially if you don't understand them / aren't aware. The flaws of async/await start becoming even more apparent when you start dealing with cancellations, and this is actually a big part of what @benlesh & others are (trying to) solve with libraries like RxJS. This is a whole topic in an of itself, and with some help from e.g. @getify & others, we could dive in deeper, but perhaps on another day.

On couple of unrelated remarks:

Thanks a lot for the writing, i learnt a lot (and i really mean it). Links provided were all instructives and imho your argumentation is strong enough that you don't need to bring in famous names to make a point (it just feels you're pushing with a crowd of names where you don't have to, really).

Cheers and thank you. I'm not putting anyone's names for the purpose of doing just that; I either mention the further arguments that I found from them that are directly related, or I bring attention that the JS community needs them to stand up, when the mentioned members are/were in favor of F# over Hack.

Also, on your TS repo comparison you may also include (for fairness) that they're implemented 1 year appart and that F# was first (and so it's most likely that it would have the most upvotes).

I am very aware and from the very start I had a paragraph explaining this already: "Of course, A [F# proposal] has been out longer, but the general consensus is pretty clear!".

Where you lost me in your demonstration is at comparing React/Angular only because you're clearly biased towards one. The comparison isn't fair since the 1st sentence of each paragraph is "Why React is great" and "The mistake that Angular made" which is not even slightly balance, and later concluded with "F# is React, Hack is Angular" to "nail the coffin". You should rather recognise that React and Angular are 2 frontend libraries not designed for the same purpose, don't cover the same scope, and their reach in functionality brings in design choices where the philosophy was different in implementation.

That's fair. I was wondering about this example too. See, I've been following Pipeline Operators from the very start, but the fact that Hack, and not F#, moved into Stage 2, I only found out 2 days ago, and thus I had to 1) catch up with everything, and 2) write this down, which means very limited time and not as well-thought-out examples & stuff.

As I wrote in part 6:

I probably shouldn't be the person writting this essay in the first place - there exist way more experienced people who could've done a better job than me. But I suppose someone is better than no-one.

Also, you lost your cool here:

Hack wants to create an extra token (e.g. ^, or %, or $_, or whatever bogus combination of symbol(s) get picked by a small group of people, effectively enforcing their choice for everyone, instead of letting the individual choose themselves), and that token also won't be usable outside the context of pipeline operators.

...which i can understand but tbh that's not with that kind of writing that you'll convince the people against it (and deciding, nonetheless) to change their mind. But that's just my opinion, and i'm a nobody - feel free to ignore :)

I still stand by this. I agree the writing could be better - I've just talked about this in the previous paragraph - limited time, and limited experience on my part. But my point still stands.

Thank you.

@lightmare
Copy link

PFA solves F#'s shortcomings and (arguably) beats the only remaining value proposition of Hack.

This is way oversimplifying it. PFA solves one issue with F#, but does not cover everything Hack can do.

// Hack pipe
x |> foo(^.bar); // PFA does not allow foo(?.bar)
x |> foo(^ + 1); // PFA does not allow foo(? + 1)

You could argue that in proper FP fashion the above would be written as:

// F# pipe
x |> member('bar') |> foo;
x |> inc |> foo;

However that's not PFA beating the value proposition of Hack — which is that one can use arbitrary expressions within a pipeline. You're just discounting the value others might see there.

Even better - Partial Application can be used outside F#'s pipeline operators, aka anywhere in the language!

Then PFA should be advancing on its own. It's on hold, because most of its strength lies in F#'s weakness.

@kiprasmel
Copy link
Author

hey @nmn, thank you!

I still think F# is better than the Hack-style proposal. My reasoning for this is not based on power, but because I prefer adding just an operator over new syntax.

<...>

I agree.

But, we already understand how function calls work and learning one new operator is simpler than learning a completely new bespoke syntax just for piping.

Exactly. I'd like to add an important detail: F# pipelines, for many people, resemble exactly that of what the bash / shell pipes are:

find . -type f | grep -E '.jsx$' | xargs sed -ibp 's/var/let/g'

Pipes in the shell enable you to compose simple utility functions together, and this is one of the foundational ways of doing shell scripting. It makes sense - it's simple yet very effective. The whole suite of utilities are designed in a way to deal with stdin coming in & stdout going out (though not necessarily), and this is what makes it so powerful. This is the F# way.

See also #206.

BUT, I don’t agree with stalling the HACK-style proposal if that is the proposal with the momentum. I’m honestly tired of waiting for this proposal and every time there seems to be a little bit of momentum an argument breaks out and progress gets stalled. I’m tired and will take the “worse” proposal if that’s what I can get.

Disagree here. As already mentioned, the Pipeline Operators are with us already - be it Functors, the pipe function, or even [].map or Promise.then -- you can already use the functionality of them.

This means that there should be no rush whatsoever to implement a solution, because 1) it's already possible, 2) the current solution (Hack) is incomplete (as argued above, and also in #200, #206 etc.).

It can also be argued that adding new bespoke syntax is how JS progresses at all. The class syntax is very bespoke and it chose to do things to look like other languages rather than try to stick to existing syntax where possible.

Maybe, but I could just as easily argue that Classes were a step in the wrong direction. It is fine, however, because it does not necessarily impact "me" - I can just use regular functions and objects, but if the Hack pipeline operator gets shipped instead of F#, you are now directly taking away a big part of functionality that others would like to use as well. See again #206.

@kiprasmel
Copy link
Author

@lightmare thanks for your input.

PFA solves F#'s shortcomings and (arguably) beats the only remaining value proposition of Hack.

This is way oversimplifying it. PFA solves one issue with F#, but does not cover everything Hack can do.

// Hack pipe
x |> foo(^.bar); // PFA does not allow foo(?.bar)
x |> foo(^ + 1); // PFA does not allow foo(? + 1)

Correct - you cannot do this with PFA.

But, you can just as easily do this:

// F# pipe
x |> y => foo(y.bar);
x |> y => foo(y + 1);

which, as I argue, can be easily seen as a benefit and yet another argument of F# over Hack. See the last 2 paragraphs of "2. The Downfalls of Hack".

However that's not PFA beating the value proposition of Hack — which is that one can use arbitrary expressions within a pipeline. You're just discounting the value others might see there.

Once again - that's F# itself beating Hack, and PFA providing convenience in some cases. I agree that others see value in Hack, but even without PFA, F#, as already argued above, feels superior in many ways. See also #206

Even better - Partial Application can be used outside F#'s pipeline operators, aka anywhere in the language!

Then PFA should be advancing on its own. It's on hold, because most of its strength lies in F#'s weakness.

Some people have interesting opinions on this. @Pokute in particular, which I've already mentioned in part "4. The JS Community":

It's unfortunate that partial application proposal is not mentioned here, since it complements F# pipelines. F# pipelines practically has the burden that partial application as part of F# pipeline proposal is so easily to separate as a distinct proposal that no-one can really object to separating it. Hack-style #-placeholders can not be separated into a separate proposal. Thus I feel like it's unfair to do comparisons where F# pipelines don't additionally have the partial application to help them. It results in a weird situation where the robustness of partial application actually hurts F# pipeline argument.

Thank you.

@mAAdhaTTah
Copy link
Collaborator

I'll start off by conceding that I mostly skimmed this post, but my initial reaction is most of this has in fact been covered before. And that's a good thing! We've investigated & poured over a lot of the problem space over the past 4 years, and the decision to advance Hack has been considered deeply.

Ironically, while you were writing this, I was writing something myself, arguing that Hack pipe is better for functional programming. This seems as good a place to share it as any:

While I originally came into the conversation advocating for the F# pipe, I’m now convinced the Hack pipe is the superior option, and that it would be beneficial to the functional community to embrace it. At core, Hack pipe is a better bridge between mainstream & functional JavaScript and makes it easier for mainstream JavaScript to adopt functional patterns.

Hack Pipe for Functional Programmers: How I learned to stop worrying and love the placeholder

@thesoftwarephilosopher
Copy link

As for cognitive load, I’d argue hack pipes are actually easier because you just mentally place the value from the left into the placeholder on the right. It’s a direct translation and nothing else.

Whereas with F# pipes you have to mentally deal with partial functions and currying which at least to me is not intuitive and difficult to mentally follow. I know for some people who get functors and monads it’s probably easier but that’s not me.

As for syntax, it’s a relatively small change. Smaller syntax isn’t automatically better and in fact that’s one reason why I was glad to move away from Clojure to TypeScript after doing it for 5 years professionally.

There may be a kind of purity to smaller syntax but we are humans and natural languages, as well as the success of JS, show that we are perfectly fine and perhaps even do better with more syntax.

I’m fully convinced that Hack pipes are mentally lighter and more pragmatic for JS devs.

@kiprasmel
Copy link
Author

kiprasmel commented Sep 13, 2021

@sdegutis you did not say anything new here, just like in your previous reply.

The cognitive load argument - everyone is just pulling it out from their ass. I've already mentioned in in the writing, AND answered multiple replies in this thread:

The cognitive load discussion between you and me, and anyone else for that matter, are just personal preferences, but the Rust community has actually done the usability research and reached conclusions that I've already mentioned above.

As for syntax, it’s a relatively small change

I began my essay by opposing this. It is not a small change. See also #206

@ghost
Copy link

ghost commented Sep 13, 2021

@sdegutis

Whereas with F# pipes you have to mentally deal with partial functions and currying which at least to me is not intuitive and difficult to mentally follow. I know for some people who get functors and monads it’s probably easier but that’s not me.

I understand that this coding pattern is idiomatic:

function double(n) {
  return n * 2
}

[7, 59, 8]
  .map(double)

I would say that passing a function reference to a HOF is a similar case as having the pipe operator expect a unary function.

37
  |> double

Then you have the freedom of dealing with functions of >1 arity by using an anonymous function expression, or by writing curried functions.

[6, 7]
  .map(n => add(n, 2))
  .map(curriedAdd(2))

7
  |> n => add(n, 2)
  |> curriedAdd(2)

That is why Hack-style seems unlike idiomatic JavaScript to me.

The desire to bypass anonymous function expressions might better be suited for the Partial Expression or Partial Application proposals, which would have the benefit of not being special cased for the pipeline operator.

F#'s function expression "syntax tax" is a language-wide phenomenon and should not be addressed with syntax special-cased for a single feature.

@shuckster
Copy link

Does it add value on either side of this argument to hypothesise an alternate history of for-loops?

for (1 .. 10) print ^ - 1

@kiprasmel
Copy link
Author

kiprasmel commented Sep 13, 2021

@mAAdhaTTah

I'll start off by conceding that I mostly skimmed this post, but my initial reaction is most of this has in fact been covered before.

This sentence is an oxymoron. How can you make such a conclusion if in fact you did not read the whole post?

Ironically, while you were writing this, I was writing something myself, arguing that Hack pipe is better for functional programming. This seems as good a place to share it as any:

Why did you write this in your own blog and not in an issue in this repository?

You take away the ability to discuss it in the place where it's mean to be - here, in the repo issues.

I highlight this problem in Q 13 in the last part of the essay.


From your article itself:

While I originally came into the conversation advocating for the F# pipe, I’m now convinced the Hack pipe is the superior option, and that it would be beneficial to the functional community to embrace it. At core, Hack pipe is a better bridge between mainstream & functional JavaScript and makes it easier for mainstream JavaScript to adopt functional patterns.

Hard disagree. You did not mention Partial Application with F#. See my excerpt of what @Pokute has commented. And I'm not going to repeat myself again - read what I wrote.

Around that time, the previous champion of the proposal, Daniel Ehrenberg, stepped down and Tab Atkins-Bittner stepped in to take his place. While Daniel favored the F# pipe, Tab favored the Smart pipe previously and the evolved Hack pipe now and as champion, had been working to bring the TC39 committee (the one that specifies JavaScript) to consensus around the Hack pipe.

Okay, so you just proved my point and answered question 11.

This is terrible.

The fact that one member, in favor of F#, stepping down, and another, in favor of Hack, stepping up, completely changes the course of where the proposal is going, is ridiculous. Of course it was not the only factor, but it is clear it had a lot to do.

Repeating Q 11:

How do you make sure both sides are represented fairly and equally?

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Sep 13, 2021

This sentence is an oxymoron. How can you make such a conclusion if in fact you did not read the whole post?

Unless you can't summarize your own arguments very well, none of the arguments you've provided here are novel. I've been working on this proposal for 4 years, and have made these arguments before. If you think you have an argument that hasn't been made yet, please direct me to it.

Why did you write this in your own blog and not in an issue in this repository? You take away the ability to discuss it in the place where it's mean to be - here, in the repo issues.

I posted it here to discuss it here, but the post is intended to be accessible to everyone, not just people active here.

The fact that one member, in favor of F#, stepping down, and another, in favor of Hack, stepping up, completely changes the course of where the proposal is going, is ridiculous.

Yes, it went from a proposal that stalled and had not advanced to a proposal that achieved consensus for Stage 2. It only looks ridiculous because it wasn't your preferred outcome.

How do you make sure both sides are represented fairly and equally?

By having several members of the champion group, some of whom advocated for F#.


The cognitive load discussion between you and me, and anyone else for that matter, are just personal preferences

They are not. If the cognitive load of having await httpGet(url) was high, you'd see a lot more code like this:

const getP = httpGet(url);
const result = await getP

But you don't, because it's not.

@thesoftwarephilosopher
Copy link

@kiprasmel

The cognitive load argument - everyone is just pulling it out from their ass. I've already mentioned in in the writing, AND answered multiple replies in this thread:

The cognitive load discussion between you and me, and anyone else for that matter, are just personal preferences, but the Rust community has actually done the usability research and reached conclusions that I've already mentioned above.

Considering it's a survey from the Rust community, there is clear selection bias towards the kind of people who use Rust, which is already a language that comes with a heavy cognitive load and relatively high learning curve.

It's not a personal preference, but a very real cognitive limitation some people have, including myself. JS is a very common-denominator language, and the Hack proposal fits perfectly with that, unlike the F# proposal.

@tam-carre

You're proving the cognitive load point with perfect examples actually. Having a partially applied function nested inside a unary function is just headache inducing for me.

I already avoid using function names by themselves in .map() like your .map(double) example above, 99% of the time writing out an anonymous function itself to avoid confusion and to make things explicit.

You may argue that this actually argues in favor of F# style since that style can write out the full function like |> x => double(x), just like .map(x => double(x) does. I'd agree that it's more consistent with these HOFs.

But I'd argue that it's more pragmatic to use Hack style:

Every time I write .map(double) and decide later I need to change it to use an explicit argument because I have some options, I have to rewrite it to .map(x => double(x, options)). This is already a DX problem I've been annoyed with regularly for the past decade straight, without interruption (for the few times I have written the name-only style first).

Whereas the Hack style is a great middle ground solution, having the best of both worlds: (a) you can avoid writing the ident and the arrow, but (b) you get the ident for free, which leads to |> double(var) which is only slightly more verbose, but with the benefit of being explicit (already a quality I require when I judge any written software), and the added benefit of being easily extensible with DX, i.e. |> double(var, options) just requires adding the new param and nothing else.

@kiprasmel
Copy link
Author

kiprasmel commented Sep 13, 2021

@mAAdhaTTah

You didn't answer this though:

"You did not mention Partial Application with F#. See my excerpt of what @Pokute has commented."

(@Pokute): It's unfortunate that partial application proposal is not mentioned here, since it complements F# pipelines. F# pipelines practically has the burden that partial application as part of F# pipeline proposal is so easily to separate as a distinct proposal that no-one can really object to separating it. Hack-style #-placeholders can not be separated into a separate proposal. Thus I feel like it's unfair to do comparisons where F# pipelines don't additionally have the partial application to help them. It results in a weird situation where the robustness of partial application actually hurts F# pipeline argument.


@mAAdhaTTah:

The cognitive load discussion between you and me, and anyone else for that matter, are just personal preferences

They are not. If the cognitive load of having await httpGet(url) was high, you'd see a lot more code like this:

const getP = httpGet(url);
const result = await getP

But you don't, because it's not.

Not a fair comparison - your example is outside the realm of pipeline operators.

Even if it were - it is still personal preference. This is the problem. It has not been studied thoroughly enough with actual users.


@sdegutis

Your whole argument of having to change the function if you want to add another argument is nullified if you consider the Partial Application proposal.

Sorry, at this point you are just cluttering the conversation.

@thesoftwarephilosopher
Copy link

@kiprasmel

I strongly feel that you and some other proponents of F# here are not actually hearing one of the main concerns I have: the cognitive load of your proposal is a bit too heavy for me and many JS users. But I don't think I can make this point any better or clearer than I have so far.

And in my experience, very often a person with very high intelligence, especially in the software and math worlds, will assume they actually have average intelligence, and not be able to recognize the intellectual limitations others have, and thus will have a very difficult time believing that the things they can understand are difficult or actually impossible for many others to understand.

For this reason I think we have been going in circles on the topic of cognitive load, and the other topics for this same reason, which I see no way out of. So I will gladly hop out of this conversation and wish you all the best in figuring this out.

@kiprasmel
Copy link
Author

kiprasmel commented Sep 13, 2021

@sdegutis dude - I completely agree with you.

The thing that I am advocating for is that the proposal needs to be carefully studied with various users to find out WHAT scenario is optimal and most intuitive, and THEN choosing the fitting proposal.

I am not saying one is better than the other. All I am saying is that we need way more sample data, rather than the clashing personal preferences of the few people that are discussing this.

@mAAdhaTTah
Copy link
Collaborator

"You did not mention Partial Application with F#. See my excerpt of what @Pokute has commented."

Partial Application was introduced at the same time as Pipe and that we have been arguing about F# paired with PFA this whole time. This is not a novel argument.

Not a fair comparison - your example is outside the realm of pipeline operators.

Exactly. It's idiomatic code pre-pipeline, and this will continue to be idiomatic code post-pipeline. Hack doesn't require a different syntactic or stylistic approach. If readability is closely associated with familiarity, choosing a syntax that looks similar to current idiomatic code is more familiar and thus more readable. Putting the await on a separate line is not idiomatic. You can't ignore real patterns in real code because it suits your preference.

@thesoftwarephilosopher
Copy link

The thing that I am advocating for is that the proposal needs to be carefully studied with various users to find out WHAT scenario is optimal and most intuitive, and THEN choosing the fitting proposal.

I assumed that's what the past 4 years of various intermittent informal discussions by committee members and their colleagues have been, both on here and in various chat rooms and maybe even in person. And all of this constitutes a relatively secure and robust sample set of data about user behavior, preferences, and thoughts, which I think is more than sufficient for the purpose. And all the points made and summarized in previous discussions, I only see being rehashed in this thread (and the last one, and the gist before that), and nothing new brought up, and no new counter arguments provided. In other words, I think the decision makers have thoroughly done exactly what you're asking for, and are ready to move on. And so am I.

@kiprasmel
Copy link
Author

kiprasmel commented Sep 13, 2021

@mAAdhaTTah

Partial Application was introduced at the same time as Pipe and that we have been arguing about F# paired with PFA this whole time. This is not a novel argument.

I am not saying this is a novel argument. I am saying that when you consider it, it makes F# more powerful, and it must be considered when comparing F# vs Hack.

@mAAdhaTTah & @sdegutis:

You can't ignore real patterns in real code because it suits your preference.

Read my previous reply to @sdegutis - it is not about my or anyone else's preference, because it will unavoidably be unrepresentative of the bigger population. I am advocating for more discussions, and more experiments with people actually trying to use the different pipeline operators, and finding what's most intuitive, and THEN choosing the superior proposal:

The committee itself does not represent the bigger population, because, 1) as you yourself @sdegutis point out:

And in my experience, very often a person with very high intelligence, especially in the software and math worlds, will assume they actually have average intelligence, and not be able to recognize the intellectual limitations others have, and thus will have a very difficult time believing that the things they can understand are difficult or actually impossible for many others to understand.

and 2) this is not how data analysis is done. By definition, a small sample is very likely to miss-represent a bigger one.


edit: see also #200 (comment)

@mAAdhaTTah
Copy link
Collaborator

I am not saying this is a novel argument. I am saying that when you consider it, it makes F# more powerful, and it must be considered when comparing F# vs Hack.

We have considered it. F# + PFA is not as powerful as Hack and doesn't address the fact that Hack composes expressions, not functions, and outside of FP style, expressions are the primary method of writing JavaScript programs.

I am advocating for more discussions, and more experiments with people actually trying to use the different pipeline operators, and finding what's most intuitive

What is done in real code is what's most intuitive. We don't have to conduct studies to know this. The entire argument for Hack pipe hinges on the fact that this looks closer to real, mainstream code than F# does, which looks to closer to FP-style code, which is a minority style in JavaScript.

@thesoftwarephilosopher
Copy link

and 2) this is not how data analysis is done. By definition, a small sample will miss-represent a bigger one.

I've never heard of this version of statistics, where data samples are inherently untrustworthy.

The committee itself does not represent the bigger population, because, 1) as you yourself @sdegutis point out:

And in my experience, very often a person with very high intelligence, especially in the software and math worlds, will assume they actually have average intelligence, and not be able to recognize the intellectual limitations others have, and thus will have a very difficult time believing that the things they can understand are difficult or actually impossible for many others to understand.

You're confusing two types of intelligence. The people I have met who work at bigger companies on bigger projects like the committee members are, typically have an ordinary level of this type of intelligence, which can deal with Functors and Monads and etc. They are very similar to me in this respect from my experience. This makes them excellent representatives of the overall JavaScript community, since they're not necessarily intellectually gifted, just on the slightly higher end of knowing and understanding typical software patterns. The fact that they're committee members doesn't automatically imply a higher level of any specific kind of intelligence, only the fact that they seem to have good judgment on software in general, and a generally agreed upon good eye to what would make software easier and better to write for common devs.

@kiprasmel

This comment has been minimized.

@runarberg
Copy link

@getify I think you are being a bit unfair. Engine developers need to deal with additional headaches in either case. In the case of F# + PFA they probably need to optimize function objects created inside pipelines and find a good debugging mechanism to dive into the call stack. But in the case of Hack engine developers must figure out a good way to represent the changing nature of the topic marker in the debugger. In either case they have some work to do. And whichever gets added to the language, I’m sure they will do a good job.

@mAAdhaTTah
Copy link
Collaborator

@runarberg Engine developers themselves expressed concerns about the F# style pipe. He is not being unfair.

@voronoipotato
Copy link

voronoipotato commented Sep 13, 2021

If pipes cant take a function they are broadly worthless to me. The whole point of pipes is that you can't dot through functions. I don't want to have a long list of lambdas, I want to have a list of functions. So if you implement it in hack style and it doesn't support the behavior demonstrated below, I will still need an operator which can do that or else my code will be parentheses soup.

x
|> addThree
|> divideByTwo

@getify
Copy link

getify commented Sep 13, 2021

@voronoipotato

...they are broadly worthless to me... I will still need an operator which can do that or else my code will be parentheses soup.

I'm accused of being unfair? I think this is example of unfair hyperbole.

Consider:

divideByTwo(
  addThree(x)
)

// compared to:

x
|> addThree(^)
|> divideByTwo(^)

The second one there is hardly "parentheses soup" compared to the first one. And the more steps you add to the composition, the more the |> improves the soup.

I understand disliking the (^) parts as seemingly unnecessary, but that's not of the same nature as what the overall proposal (either style) is trying to fix, which is the NESTING of function calls inside other parentheses-call sets.

I understand that you probably are claiming you never pipe/compose with anything but already unary functions, but... a lot of people are also contemplating the downside of F# (without PFA) of needing to add (at least) x=> to non-unary function calls.

I would assert that those two competing concerns/downsides ((^) vs x=>) in some respects wash out. I think that's why most F# proponents here cite PFA (no guarantees of concurrent standardization, btw), because then the x=> tax goes away.

But... as I said in my previous message, I don't think PFA's solution there is "free". I dislike its cost, for different reasons.

My point is, there's no "clearly this is superior" and "clearly this is inferior" here... we all know both have some upsides and some downsides. The hyperbole of "this one is useless for me" or "this one is absolutely perfect for me" doesn't really advance our discussion any.

@kiprasmel
Copy link
Author

kiprasmel commented Sep 13, 2021

@getify

I think that's why most F# proponents here also want PFA (no guarantees of concurrent standardization, btw), because then their x=> tax goes away.

Yes, that's why PFA is wanted.

The fact that it isn't guaranteed as a concurrent standardization, is still fine, because F# pipes would be useful on their own.

But, this cannot be used as an argument of Hack being better than F#, because Hack effectively is a combination of F# + PFA (arguably a worse one), and the fact that it's easy to separate out the PFA proposal from the F# proposal (unlike Hack, where it's impossible, because Hack without ^ by definition is useless / does not work) is an unfair advantage when doing comparisons.

This was pointed out very early by @Pokute:

It's unfortunate that partial application proposal is not mentioned here, since it complements F# pipelines. F# pipelines practically has the burden that partial application as part of F# pipeline proposal is so easily to separate as a distinct proposal that no-one can really object to separating it. Hack-style #-placeholders [^-placeholders] can not be separated into a separate proposal. Thus I feel like it's unfair to do comparisons where F# pipelines don't additionally have the partial application to help them. It results in a weird situation where the robustness of partial application actually hurts F# pipeline argument.

@getify
Copy link

getify commented Sep 13, 2021

But, this cannot be used as an argument of Hack being better than F#

I DID NOT make that claim. I said that (^) vs x=> mostly washes out.

What I am claiming is that even though F#+PFA removes the x=> (and there's no corresponding reduction of (^) for Hack), that this is not a strong argument in favor of F#+PFA, because I think there are other costs to consider with PFA that aren't as obvious as characters used.

@shuckster
Copy link

Is the issue here really the name of the operator? Is |> ^ "chain" and -> "pipe" per Proposal 3?

Even if Hack is the only game in town for now, can we keep the nomenclature away from the Bash/FP prior-art?

@voronoipotato
Copy link

voronoipotato commented Sep 13, 2021

To be clear, I won't be using it if it has this ^ syntax. My coworkers are going to be asking me why we're using xor. At least with the simpler pipes it's actually using something that already exists in javascript. It's really a needless complication to something that should be dead simple. By contrast if we break out ^ to a separate feature, I can use pipes, and you can use ^. Passing functions as values is very vanilla javascript, by contrast ^ is magical.

@getify
Copy link

getify commented Sep 13, 2021

Is the issue here really...

Not in my opinion. I think there are substantive issues, not just naming.

Hack style does not require the special handling of await as if it were a function. I consider that a substantive downside to F#+PFA.

@kiprasmel
Copy link
Author

@getify

I DID NOT make that claim. I said that (^) vs x=> mostly washes out.

I did not say you made that claim, I am highlighting a point related to what you said.

What I am claiming is that even though F#+PFA removes the x=> (and there's no corresponding reduction of (^) for Hack), that this is not a strong argument in favor of F#+PFA, because I think there are other costs to consider with PFA that aren't as obvious as characters used.

Why is it not a strong argument in favor of F# + PFA?

The reasons you list out are very valid (1. F# + PFA removes x =>, and 2. there's no corresponding reduction of (^) for Hack), and the only considerable thing that remains is the expressions vs unary functions debate (also await, which has already been discussed #205 (comment)).

That (expressions vs unary functions) I'll cover soon.

Can you clarify:

there are other costs to consider with PFA that aren't as obvious as characters used.

what are these costs exactly, and how is Hack's ^ not an even worse cost?

@getify
Copy link

getify commented Sep 13, 2021

what are these costs exactly

Please see this comment. Also, special casing of await / etc -- see below.

@runarberg
Copy link

@getify

Hack style does not require the special handling of await as if it were a function. I consider that a substantive downside to F#+PFA.

It remains to be seen (and will probably never be realized) whether this is an actual issue for potential users. No usability studies were conducted before it was claimed this was a downside, and since #204 was closed, no studies were ever intended. It was simply decided by a committee that a special case handling of await was bad.

@getify
Copy link

getify commented Sep 13, 2021

also await, which has already been discussed ...

I don't see anywhere in this thread (which I have read all of), including the comment you linked to, where the concerns I have, which relate to conceptual cohesion and thus teachability, about special casing things like await have been addressed.

@getify
Copy link

getify commented Sep 13, 2021

It remains to be seen (and will probably never be realized) whether this is an actual issue for potential users.

That's one angle to consider the concern, but I have a different angle, which I just stated -- the conceptual cohesion and teachability. I've repeatedly referred to this concern in this thread. Thus far, I don't see it having been addressed in a substantive (non-dismissive) way.

@runarberg
Copy link

Teachability is also measurable and the teachability of the special case of await has never been measured as far as I know. For all we know stating that: “Even though await is usually used as a prefix, it can sometimes be used as a postfix if used inside pipeline” might be good enough for students to realize and grasp the special case.

@voronoipotato
Copy link

That's one angle to consider the concern, but I have a different angle, which I just stated -- the conceptual cohesion and teachability. I've repeatedly referred to this concern in this thread. Thus far, I don't see it having been addressed in a substantive (non-dismissive) way.

I think that's a very interesting point, but it's the same if you want to await in a bunch of nested functions, so what you're proposing is a solution to an existing problem with or without pipes, similar to the placeholder.

@kiprasmel
Copy link
Author

kiprasmel commented Sep 13, 2021

@getify

what are these costs exactly

Please see this comment.

Okay.

You like fn(^, 2) over fn(?, 2) because the second creates a partially applied/curried function, which you suspect would show up on the stack and is also supposedly a concern for engine optimization.

And that's fair. But does that really outweight the benefits that F# + PFA have over Hack?

Did I miss anything else? (Apart from await which I do not agree with since there's Promise.then, as already discussed:

@samhh

This has already proven itself a mistake in my opinion. Whereas other languages like Haskell have a generalised notion of a monad, giving you do notation for free with any monad, we opted to ignore well-trodden ground in the FP community and as a result have needlessly specialised async/await syntax

)


Coming back to the engine stuff, @mAAdhaTTah replies that:

Engine developers themselves expressed concerns about the F# style pipe.

3 questions to @mAAdhaTTah, or anyone else for that matter:

  1. I might have just missed this, so pardon my ignorance, but I'm seeing this for the very first time and it's concerning that this hasn't been (?) mentioned before. Has it? And where? And if it isn't linked directly in the README then why?

  2. What are the concerns that engine developers have expressed?

As @runarberg replies:

Engine developers need to deal with additional headaches in either case. In the case of F# + PFA they probably need to optimize function objects created inside pipelines and find a good debugging mechanism to dive into the call stack. But in the case of Hack engine developers must figure out a good way to represent the changing nature of the topic marker in the debugger. In either case they have some work to do. And whichever gets added to the language, I’m sure they will do a good job.

  1. Wouldn't it be the case that if you really cared about performance, you'd transpile/compile/do whatever to turn your source code into simpler JS where pipes would be de-sugared anyway?

@getify
Copy link

getify commented Sep 13, 2021

I'm not arguing for it to NOT be studied. I think it should be. But the lack of it being studied thus far, or at least seriously explored, is not evidence in favor of either style.

Hack doesn't suffer from any potential weak outcomes in this area, whereas F#+PFA might or might not. That fact alone is only mild/weak signal in favor of Hack.

But my experience teaching these sorts of things gives me pause, where I wouldn't prefer any proposal to advance just assuming it's not a problem. That's only anedoctal evidence, and I suspect nobody here gives much weight to my skepticism. But that is why I landed on the side of Hack, and it's a part of why I'm still there even with all this discussion.

@tabatkins
Copy link
Collaborator

I'm not going to be reading this entire thread (it's, at the time of me posting this, 85 screen-heights worth of text on my monitor, posted in just 22 hours since the thread was started), but I see several comments on this thread that are insulting or dismissive, and either violate TC39's CoC or skirt close to it. I won't tolerate "discussion" of this type, and will be deleting comments as necessary, and possibly banning users from this repository, if things cannot remain civil. This is the only warning.

@voronoipotato
Copy link

Sorry if I got heated. I thought by positioning it as two separate features we could both get what we wanted. One side could have pipes as they are familiar and the other side gets placeholders as they are used to. I am not opposed to hack style placeholders, I just didn't want them to be the exclusive way things are done. Again I apologize for getting a bit polemic, I felt like it was being proposed as an either|or even when I was explicitly trying to include other approaches, but I should have been more patient. Thank you for helping @tabatkins .

@js-choi
Copy link
Collaborator

js-choi commented Sep 13, 2021

Engine developers themselves expressed concerns about the F# style pipe.

For what it’s worth, @syg of Google V8 expressed some thoughts in the 2021-06 pipe incubator meeting, before the 2021-08 plenary meeting:

SYG: I'm more in favor of Hack style in that linearization for partial application + F# seems much harder to optimize with a proliferation of intermediate arrow functions. I think linearization is going to be reached for often if it becomes a thing, so it's important to optimize this in the frontend without depending on the JITs. With F# + partial app depending so much on intermediate functions, it's probably not as optimizable in the frontend which would be a shame.

I believe that @codehag of Mozilla SpiderMonkey has expressed similar concerns offline, mentioning that people generally overestimate how much optimization engines can do with regards to things like arrow functions in F# pipe bodies (from what I recall, she’s actually slightly against any pipe operator). I don’t have a source for that second quote, though, and my memory is fallible, so please take that one with a grain of salt.

@mAAdhaTTah
Copy link
Collaborator

I might have just missed this, so pardon my ignorance, but I'm seeing this for the very first time and it's concerning that this hasn't been (?) mentioned before. Has it? And where? And if it isn't linked directly in the README then why?

It's in the incubator notes.

SYG: I'm more in favor of Hack style in that linearization for partial application + F# seems much harder to optimize with a proliferation of intermediate arrow functions. I think linearization is going to be reached for often if it becomes a thing, so it's important to optimize this in the frontend without depending on the JITs. With F# + partial app depending so much on intermediate functions, it's probably not as optimizable in the frontend which would be a shame.

Wouldn't it be the case that if you really cared about performance, you'd transpile/compile/do whatever to turn your source code into simpler JS where pipes would be de-sugared anyway?

  1. You can't transpile away the intermediate function returned by curried libraries like Ramda.
  2. Why do something with 2 tools that you can do with 1?

@getify
Copy link

getify commented Sep 14, 2021

But does that really outweight the benefits that F# + PFA have over Hack?

That, plus the special handling of await (which I still consider an issue, though I know you don't) is just enough to sway me toward Hack.

@nmn
Copy link

nmn commented Sep 14, 2021

Reading @getify’s comments, I’d like to summarize the state of things as follows: (please correct me if I get anything wrong)

  1. One downside of the F#-style proposal is the need for intermediate function calls, while Hack-style can treat things as a sequence of expressions. The argument was made that this would make F# have worse performance.
  2. The cost of (^) vs x => mostly cancels out. While I agree in that the amount of characters to write is not the main issue in favor of either proposal, F# is using existing arrow function syntax, while Hack is introducing new one-off syntax.

My thought here is that while you may say that F# is abusing the arrow function syntax, I think it’s a strength that it is using the existing syntax. After a lot of thinking and explanations by Tab Atkins, I no longer think there is any “power” that on proposal has that the other one lacks. They are both fully-featured (or can be).

So my thoughts boil down to the fact that I think adding bespoke new syntax for single new feature is counter-productive and I’ve yet to see any arguments from supporters of Hack-style on why adding new syntax for just this one feature is a good idea.

I could see a good argument for why new syntax is good:
New syntax builds new patterns that will have parallels for different features.

Example: Generator functions were added to JavaScript and they introduced new bespoke syntax. This was a very good design because:

  1. They used syntax similar to return statements and not something completely new.
  2. The same syntax was later used for async-await as well.

Could anyone give me examples of how the new syntax introduced for Hack-style will fit into the larger syntax of the language?

f# is just an operator, which I consider a strength.

@aadamsx

This comment has been minimized.

@getify
Copy link

getify commented Sep 14, 2021

@nmn

I’ve yet to see any arguments from supporters of Hack-style on why adding new syntax for just this one feature is a good idea.

It buys us the ability for expressions in the pipe to use things that a function boundary (like an inline arrow function wrapped around expression body) would obscure, such as using await or yield.

// Hack:
fn()
|> await Promise.race([ ^, cancelToken ])
..

IIUC, that's not directly possible (or, at least, as ergonomic) with the F# style pipes, even with PFA, because you have to wrap that await .. expression in an arrow to get named access to the passed in value, but then await isn't valid inside of that arrow function. I think you instead have to do something like (my apologies if there's an easier way that I missed):

// F#
fn()
|> v => Promise.race([ v, cancelToken ])
|> await
..

In fact, for a simpler example, where it would just be await ^ in Hack style, the F# proposal suggests special handling that treats await and yield like they are unary functions:

// Hack:
fn()
|> await ^
...

// F#
fn()
|> await
...

If you compare those two versions, the conceptual "magic trick" that has to happen to treat await like a unary function is not, IMO, worth saving that extra ^ character.

So yeah, IMO there are definitely pathologic cases where F# (even with PFA) gets more worse than the worst case in Hack, which is that you have to add (^) (or just ^).

To answer your question directly, I think avoiding those cases and magic is worth the invention of the ^, even with the other costs as noted.

@sandren
Copy link

sandren commented Sep 14, 2021

I'm not going to be reading this entire thread (it's, at the time of me posting this, 85 screen-heights worth of text on my monitor, posted in just 22 hours since the thread was started), but I see several comments on this thread that are insulting or dismissive, and either violate TC39's CoC or skirt close to it. I won't tolerate "discussion" of this type, and will be deleting comments as necessary, and possibly banning users from this repository, if things cannot remain civil. This is the only warning.

@tabatkins I hope you don't let a few unpleasant comments from a few particular people dissuade you from reviewing the other well thought out and articulated discussion in this thread.

I understand that it may be a lot to review at once and keep up with, but many of us have, and we are all hoping that you will extend the same courtesy to review our concerns as well.

We appreciate it. 🥰

@tabatkins
Copy link
Collaborator

This thread had, at the time I posted that comment, received a post on average every six minutes for twenty-two straight hours. For anyone who didn't livestream the thread, it's far too long to read. I've been working in standards for over a dozen years now, and participated in my share of centi-threads, and let me tell you - they're never worth reading (even if I'm the one responsible for them!).

I did, fwiw, read the OP beforehand - it doesn't contain any information that wasn't already expressed in one of the multitude of threads talking about F#-style pipelines over the last three years. I also, after posting my last comment, did actually skim the thread in reverse to check for posts that deserved to be proactively hidden; this continued to be true. We've had these discussions over and over; the points have been made, the arguments have been considered, and the current proposal is where we've ended up after all of that.

Expressed in this very thread are comments from several people who feel they've been talked over and spoken for, and that expressing any opinion that is not pro-F#-style will provoke a hostile pile-on. They're not wrong, and that's unacceptable.

I'm going to go ahead and close this thread; it appears to have run its course, and in any case is filled with unacceptable levels of hostility. As I've said in previous threads here, new information is welcome, or arguments one feels were unfairly passed over. But this repo has three years of arguments on this exact subject already; please review those before asking the rest of us to read twenty-three thousand words in one new thread.

I will continue to close threads that do not meet the minimum guidelines from our CoC to be respectful, friendly, and patient. If necessary, further moderation action may be taken; I pray it doesn't become necessary.

If you still feel like you'd like to talk to my manager, here's the list of chairs, here's the CoC, and you can most easily get in touch with people via TC39's Matrix rooms

@tc39 tc39 locked as too heated and limited conversation to collaborators Sep 14, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests