tc39 / proposal-pipeline-operator

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

Effect of Hack proposal on code readability #225

Closed arendjr closed 2 years ago

arendjr commented 2 years ago

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

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

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

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

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

ken-okabe commented 2 years ago

Actually, it's a bad manner to tweak the binary operator itself for any purpose that essentially breaks the algebraic structures, and without messing up the math, we can compose "advantage" with the basic operator if we want to.

Mathematically, *HackStyle = MinimalStyle PartialApplication("advantage" you feel)* where `` is an operator of function composition.

@kiprasmel made an excellent presentation, did you read? https://github.com/tc39/proposal-pipeline-operator/issues/202#issuecomment-917626903

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

Another insight https://github.com/tc39/proposal-pipeline-operator/issues/205#issuecomment-918370726 @SRachamim

Hack style is a bad proposal that tries to combine two great proposals: F# |> + ? partial application. For me there's no question. F# is the only way to go.

In software design, we must know: One Task at a Time Code or a function should perform only a single task and use function composition image

or more generally, KISS principle

When we know the two are equal:

The latter should be chosen for robustness, and finally,

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

I've just posted Concern on Hack pipes on algebraic structure in JavaScript #223 This issue actually mentions the impact to readability, so please read if you have a time.

arendjr commented 2 years ago

@stken2050 Thanks for pitching in. So what I got from your post is that the usage of lambda expressions on the RHS of the pipe operator would open up the possibility of accidentally referencing variables that happen to be in scope, and in the worst case developers might try to abuse that scope for implicit state management between invocations. That's indeed a good argument against the usage of lambdas there, and it is also another argument against the Hack proposal specifically, as its ability to embed arbitrary expressions makes the likelihood of such maintainability issues even larger. Hope I understood correctly :)

ken-okabe commented 2 years ago

So what I got from your post is that the usage of lambda expressions on the RHS of the pipe operator would open up the possibility of accidentally referencing variables that happen to be in scope, and in the worst case developers might try to abuse that scope for implicit state management between invocations.

Actually, yes, and I'm very sorry about that I misunderstood your context.

In any way, I firmly believe if we pursuit some advantage on top of the operator, we should compose it. First we provide the minimal and pure operator |>, then using the operator we add on advanced features, which may become |>>.

mAAdhaTTah commented 2 years ago

@arendjr One clarification from your blog post: the Hack proposal doesn't introduce +> as part of the current proposal. That's a potential follow-on proposal. Hack also doesn't kill the partial application proposal, which could advance as an orthogonal proposal (see #221 for some info on its advancement issues).

Otherwise, the only only somewhat-amusing note is I find all of your examples with Hack pipe an improvement over the status quo versions. The only one I'd speak to specifically is the npmFetch version, which I much prefer over the baseline. If that was broken onto multiple lines, rather than one single line, it would separate the data transformation work being done in the first two steps from the API call on the third step in a way I find much clearer & easier to understand.

Beyond that, I appreciate the examples & exploration. clip is a great example of an API written in more mainstream idiomatic JavaScript that would be harmed by the F# proposal, as it would require either a pipe-specific API or arrow function wrapping.

arendjr commented 2 years ago

@arendjr One clarification from your blog post: the Hack proposal doesn't introduce +> as part of the current proposal. That's a potential follow-on proposal. Hack also doesn't kill the partial application proposal, which could advance as an orthogonal proposal (see #221 for some info on its advancement issues).

Yes, good clarification!

Otherwise, the only only somewhat-amusing note is I find all of your examples with Hack pipe an improvement over the status quo versions. The only one I'd speak to specifically is the npmFetch version, which I much prefer over the baseline. If that was broken onto multiple lines, rather than one single line, it would separate the data transformation work being done in the first two steps from the API call on the third step in a way I find much clearer & easier to understand.

For that example specifically, I would rather write it like this:

const { escapedName } = npa(pkgs[0]);
const json = await npmFetch.json(escapedName, opts);

No pipe necessary at all, and in my opinion it’s clearer than both options given in the example. It also achieves the separation between processing and actual fetching you were aiming for.

And this is getting to the heart of why I feel the Hack proposal might be actively harmful. It encourages people to “white-wash” bad code as if it’s now better because it uses the latest syntax. But not only do I not find it an improvement, we have better options available today.

And people will mix and match this with nesting at will. It will not rid us of bad code, but it will open up new avenues for bad code we had not seen before. That’s why in the end I think we’re better off without than with.

For the F# proposal, I don’t see so much potential for abuse, hence why I’m still sympathetic to it. But if we could prohibit lambdas from even being used with it, I would probably be in favor. It’d make it even less powerful, but I do believe this is a case where less is more.

Beyond that, I appreciate the examples & exploration. clip is a great example of an API written in more mainstream idiomatic JavaScript that would be harmed by the F# proposal, as it would require either a pipe-specific API or arrow function wrapping.

Yeah, absolutely. I would gladly make the adjustment if the F# proposal were to be accepted, but it’s true it’s a pain point. I do think once libraries had their time to adjust, we might come out for the better, but I cannot deny it will cause short-term friction.

If the Hack proposal were accepted however, things might initially appear more smooth. But once we have to deal with other people’s code that uses pipes willy-nilly, I fear we may regret it forever.

mAAdhaTTah commented 2 years ago

And this is getting to the heart of why I feel the Hack proposal might be actively harmful. It encourages people to “white-wash” bad code as if it’s now better because it uses the latest syntax. But not only do I not find it an improvement, we have better options available today.

And people will mix and match this with nesting at will. It will not rid us of bad code, but it will open up new avenues for bad code we had not seen before. That’s why in the end I think we’re better off without than with.

This is a reasonable take, even if I disagree with the cost/benefit calculation.

For the F# proposal, I don’t see so much potential for abuse, hence why I’m still sympathetic to it. But if we could prohibit lambdas from even being used with it, I would probably be in favor. It’d make it even less powerful, but I do believe this is a case where less is more.

I think this makes the use-case for F# unreasonably narrow which would really limit the utility of the operator.

Yeah, absolutely. I would gladly make the adjustment if the F# proposal were to be accepted, but it’s true it’s a pain point. I do think once libraries had their time to adjust, we might come out for the better, but I cannot deny it will cause short-term friction.

My general perspective is forcing mainstream JS to adapt to this API will be more painful than asking functional JS to adapt to Hack. Note that if you were to adapt to the curried style you reference in the blog post, you would no longer be able to use clip outside of a pipe without a very odd calling convention (clip(length, options)(text)) or using a curry helper.

arendjr commented 2 years ago

Note that if you were to adapt to the curried style you reference in the blog post, you would no longer be able to use clip outside of a pipe without a very odd calling convention (clip(length, options)(text)) or using a curry helper.

My idea was to just check for the type of the first argument, if it's a number, return a lambda. If it's a string, continue as before. It's JS so why not :) But alternatively, I could just ask the FP folks to use import { clip } from "text-clipper/fp" and expose the new signature there.

SRachamim commented 2 years ago

I'm still surprised to see claims against the minimal/F# style, saying that it will force curried style. It won't, just as .then and .map won't. Why don't we introduce ^ in the then and map methods as well?

PFA proposal is a universal solution to those who worries about the arrow function noise on all three cases: map, then and pipe. If PFA is not ready, and we don't want minimal/F# without PFA, then let's wait, instead of introducing an irreversible Hack pipes.

Pipe, in any resource you'll find, is defined as a composition of functions. From UNIX to Haskell, that's what it is. I really think we should hold that proposal ASAP and continue discussing it.

mAAdhaTTah commented 2 years ago

@SRachamim The loss of tacit programming is explicitly a complaint against Hack (see #215 and #206). If the intention is to just use lambdas for everything, then F# is at a significant disadvantage vs Hack.

See #221 for concerns about PFA's advancement.

shuckster commented 2 years ago

I think it's worth trying to define what "readability" can mean across paradigms and across time.

Functional Programmers worry about mathematical consistency: g -> f is the same as f(g).

You really don't have to be on one side or the other to appreciate that Hack robs the JavaScript FP community of a certain kind of transferable mathematical thinking. "Readability" to them means something very different to "avoiding temporary variables". F# affords another kind of functional compositional to JavaScript.

Should it have it? JS is of course a multi-paradigm language, but despite the currently declarative push it's no secret that it's still very much used in an imperative way, so perhaps Hack suits it "better"?

But JavaScript also has -- by pure luck of history thanks to its original author -- first-class functions. It's baby steps away from being completely FP friendly, and it has an active FP community that seems acutely aware of this.

I'm really trying to avoid falling on one side or the other here, but after that throat-clearing I contend that:

In other words, Hack is the readability ceiling for imperative programming, whereas F# raises the ceiling for compositional readability for those working in FP, either now in in the future.

SRachamim commented 2 years ago

@SRachamim The loss of tacit programming is explicitly a complaint against Hack (see #215 and #206). If the intention is to just use lambdas for everything, then F# is at a significant disadvantage vs Hack.

See #221 for concerns about PFA's advancement.

Lambda is a (apparently scary) name to something that we're all comfortable with: a function. It's already everywhere: class methods are functions, map, filter and reduce, then. This is a simple concept. But more importantly, it's the essence of the pipe concept.

Every concern you'll have about minimal/f# proposal can be applied to anywhere else you use a function. So why won't we tackle it all with a universal PFA solution?

Why won't we use promise.then(increment(^))) instead of promise.then(increment)? You see, the Hack style is a verbose proposal that tries to tackle two things at once.

And as I said, if PFA is stuck, then it's not a good reason to introduce Hack. We should either wait, or avoid pipe at all (or introduce minimal/f# style anyway).

mAAdhaTTah commented 2 years ago

Functional Programmers worry about mathematical consistency: g -> f is the same as f(g).

Does this imply that Elixir isn't mathematically consistent because x |> List.map(fn num -> 1 + num end) is the same as Enum.map(x, fn num -> 1 + num end)?

voronoipotato commented 2 years ago

Elixir pipes allow you to take a lambda or function. Whether it pipes to the first value or the last value is an interesting stylistic question, and I get what you're referring to. In javascript our functions can be thought of as taking a tuple. This adds the wrinkle of, are you injecting into the first value of the tuple, or the last since there is an isomorphism between (a b c) -> d and a -> b -> c -> d. The elixir is still mathematically consistent in my opinion with the general idea, because we have various flip functions which could if need be take an a -> (b c ) -> d and turn it into (b c) -> a -> d . So they're algebraically equivalent, so we don't really need to worry about it. If we get the a -> (b c ) -> d , we can just use a flip function to get (b c) -> a -> d and vice versa.

What is a concern is whether I have to learn a new construct to reason about the new operator. The functional community will simply avoid placeholder pipes, because they're pipes that don't take functions, and we think in terms of passing functions around. You can call it superstitious or close minded, but it is what it is. I don't think the fp community will ever use placeholder pipes because they are difficult for us to reason about in terms of edge cases. I actually agree with @arendjr in that I would prefer to have no pipe rather than hack pipes, and many many functional devs I have talked to this week have agreed with that. As excited as we are about pipes, we want to avoid things which are going to make code harder to reason about the potential consequences and read. Functions and lambdas are easy to understand and read, expressions by contrast are very open ended in what they can accomplish and what they mean. Expressions are very powerful but we fp devs often explicitly trade power for the ability to think clearly. In my opinion javascript is already very open ended, and some creative constraints can make things easier to use.

mAAdhaTTah commented 2 years ago

Whether it pipes to the first value or the last value is an interesting stylistic question, and I get what you're referring to.

Yeah, my general point is that whether the syntax of a given language specifically matches the syntax of the math is less important than whether it adheres to the values/principles that math.

we think in terms of passing functions around.

Nothing about Hack pipe prevents you from passing functions around. x |> f vs x |> f(^) is immaterial to the underlying math, and is immaterial to your desire to just do function passing. x |> f implies that f is called; x |> f(^) makes that difference explicit. It doesn't change the actual behavior.

Functions and lambdas are easy to understand and read, expressions by contrast are very open ended in what they can accomplish and what they mean.

The body of a lambda, specifically arrow functions in JavaScript, is an expression. Most of the open-ended things you can do with Hack pipe you can do in the body of a lambda (save await / yield) Hack enables you do that in the same scope as the pipeline, instead of the nested scope of the lambda/arrow. Neither pipe prevents you from using expressions, but because expressions are so central to the language, enabling their ease would be significantly more beneficial to the broader language & ecosystem than it will be harmful to your ability to chain together function calls.

arendjr commented 2 years ago

Neither pipe prevents you from using expressions, but because expressions are so central to the language, enabling their ease would be significantly more beneficial to the broader language & ecosystem than it will be harmful to your ability to chain together function calls.

[Citation needed] I’m literally arguing the other way around. I even proposed it might be better to even limit F# pipes from piping into lambdas, to prevent exactly this (which you rejected). I’m okay if you want to argue that this gives neither pipe proposal a reason to exist, but please don’t use this as an argument for why Hack would supposedly be better.

voronoipotato commented 2 years ago

The problem is that in real life I have to deal with other people's code, and expressions are pretty open ended. If you constrain expressions to exactly what a lambda does, that's great but now I have a lambda that is not obvious that it's a lambda, it's confusing. Therefore, yes, in my codebase with myself alone hack pipes are fine, but in real life I have to deal with my peers, and I will tell them "Please avoid the placeholder pipes, they are confusing and may not work as you think" because people in practice will do all kinds of stuff that are, well confusing. My problem with placeholder pipes is explicitly that they are too open ended. The very thing you espouse as a killer feature, makes it to me a DOA feature. I still after all this discussion for example, don't know if anyone fully understands the scope of a expression pipe. If my coworker declares a var in an expression pipe, is that global, or local like a function? Will my coworker know? They will not, because I don't know, and neither will most people. It's an entirely new construct with new expectations that could be frankly anything. It's why I don't think this proposal works well for javascript, because it's actually subtly complicated and javascript is already quite complex. Remember even if you can give me a nice succinct answer to this question, consider that people will forget, because it's unintuitive to have expressions have a scope. Do those placeholder expressions have a this? Can I add attributes to it or treat it like an object? There's so many ways that people will use placeholder pipes to make weird cursed code and we both know it. By contrast with function pipes I can just refactor any sufficiently cursed lambda to a named function and know that the scope will be exactly the same, period. If it's a lambda, I can just say, "It's a lambda" and they get it. I don't think awaiting mid pipe is actually a good feature, and I think it will just be hard to read because awaits normally go at the beginning of the line. So @arendjr if you're worried about cursed lambdas, they would be trivial to refactor into named functions. Probably even a action in vscode and your favorite editor to do that. Whether such a thing is even strictly possible in expressions is frankly unanswerable, especially when you consider the scope of all the things which may be added yet in the language.

mAAdhaTTah commented 2 years ago

[Citation needed]

All of the built-in types, all of the Web apis, none of them are designed around unary functions. Even tools as basic as setTimeout are going to require you to wrap it in a lambda in order to use it in F# pipes. If we're trying to introduce new syntax to the language, requiring all of those APIs to wrapped in functions to be useful significantly impairs the usefulness of the operator.


If my coworker declares a var in an expression pipe, is that global, or local like a function?

It would be a SyntaxError because var is a statement, not an expression.

SRachamim commented 2 years ago

@mAAdhaTTah You are worried about unary functions on a pipe, but why aren't you worry about unary functions on every other method like then? Why it's ok to accept a lambda there?

If you worry about unary functions, why not pushing the PFA proposal, which will allow you to use setTimeout without a lambda on both a pipe and a then (and actually everywhere).

This claim is not a reason to push the Hack proposal.

mAAdhaTTah commented 2 years ago

Why it's ok to accept a lambda there?

then is a method; it's not new syntax.

If you worry about unary functions, why not pushing the PFA proposal

I already referred you to #221 to discuss the issues PFA had advancing.

SRachamim commented 2 years ago

@mAAdhaTTah New syntax must not imply inventing new unexpected ways of reasoning. A great syntactic sugar is one which leverages existing familiar constructs. You can describe the minimal proposal as a |> f === f(a). It's simple, and doesn't require anyone to learn anything new. It's just |>. Compare it with the Hack proposal which is entirely new concept not only in the JavaScript world, but in programming in general!

And again, some variation of PFA is the solution to your concern about curried/unary functions. Don't leak it to other proposals. Where PFA proposal stands should not affect the nature of a pipe.

Pipe should be a simple function composition, whatever function means in JavaScript. If you feel functions in JavaScript need another syntactic sugar - that's a different proposal!

arendjr commented 2 years ago

All of the built-in types, all of the Web apis, none of them are designed around unary functions. Even tools as basic as setTimeout are going to require you to wrap it in a lambda in order to use it in F# pipes. If we're trying to introduce new syntax to the language, requiring all of those APIs to wrapped in functions to be useful significantly impairs the usefulness of the operator.

You’re still making the assumption that it is desirable to call anything and everything from pipes, where I think I made it abundantly clear that is not a desire in the first place.

What you’re suggesting is a new syntax for any call expression in the language, regardless of asking the question whether we should want that. Adding two arbitrarily interchangeable syntaxes for the same is not a benefit to me, it just complicates things. It complicates the language for newcomers, it adds endless discussion about which style should be preferred when and where, it promotes hard-to-read code for which better alternatives exist today and ultimately it has very little to show for it.

Someone on Reddit replied to me:

“I don't look forward to trying to debug a broken npm package and see it's written with pipes for every function call.”

And yet that seems to be exactly what you’re encouraging.

So I did ask this question to myself, and I think that No, we should not want this.

And yet I keep reading statements from champions making sweeping generalizations that this is “beneficial to the broader language” or “beneficial to everyone” and I don’t feel represented by this. And frankly I suspect there might be a large underrepresented, silent majority that will not feel their concerns were represented if this proposal is accepted.

voronoipotato commented 2 years ago

I am confident there are a lot of unexplored edgecases of expressions which my coworkers or teammates in projects will use, and abuse. For example okay so there are expressions not statements, I get it now, what is this? What about super ? What about properties? If we consider the this scope of a pipe, is that even well defined in the spec? There's just countless unanswered questions in my opinion, and what we gain is the ability to write riddles to punish our future selves for hubris. Not to mention any time from here on after we have to carefully consider any expression being added to the language, how that should work with placeholder pipes, what it can and cannot do. Placeholder pipes are in my opinion, to be avoided, as we should not trust that future features will behave well with them, because the design space is far too large and novel. An awkward case I just thought of with expression pipes is setting a property, which is an expression, so then what happens is the value set gets piped through, counterintuitively.

var x = 10 |> this.whatever = 'yeah'
// x is 'yeah' and yeah you can easily forget to put in the placeholder entirely and imo it is not at all obvious. 
var y = {lets: 'go' } |> ^.lets = 'hooray'
//y is 'hooray' and is no longer an object

This is extremely unintuitive, and will definitely blend the mind of a newbie into fresh paste. By contrast, in a function or lambda it's all about returning stuff, there's a clearly communicated path, you can even use braces and a return statement to be explicit (and I often do). In the first example, I don't even know what I'd expect, but probably either 10 to be passed through unaffected, or undefined. You better believe my coworkers not understanding pipes will put every valid expression in there. At least I kinda understand functions, they are simpler, expressions can be so many things. There is a pre-existing guidance on how/when to use functions, I have no such guidance for placeholders.

noppa commented 2 years ago

var x = 10 |> this.whatever = 'yeah'

From the readme

A pipe body must use its topic value at least once. For example, value |> foo + 1 is invalid syntax, because its body does not contain a topic reference. This design is because omission of the topic reference from a pipe expression’s body is almost certainly an accidental programmer error.

So it would be pretty difficult to have this mistake go unnoticed. Any sort of linter would point it out and trying to run the code would just crash with a syntax error.

var y = {lets: 'go' } |> ^.lets = 'hooray'

I don't see how the F# version is any different

var y = {lets: 'go' } |> _ => _.lets = 'hooray'

I'm also wondering where yall find these crazy coworkers that bend over backwards to write as unintelligible code as possible, and can't be leveled with.

voronoipotato commented 2 years ago

The difference is people expect to intend to return a value with a lambda. What value are you intending to return there? By contrast an expression is a lot of things. The root of it is, you're treating two very different things like they're equivalent. I guarantee you when this gets released we will see ^.lets = 'hooray' on stack overflow with placeholders, and we won't with lambdas, because lambdas have been in the language for several years now, and function expressions several years before that. People have a general intuition of what is expected with function expressions and lambdas, that will have to be learned with placeholder expressions.

Specifically, they're your bootcamp grads, fresh out of college kids, mom and pop shops, and sometimes your boss who mostly writes Java/C# but thinks javascript is easy. You can explain things to them, but then someone new will come with the same misconception, or perhaps the same person who forgot, so I'd really appreciate it if we make sure to value making things intuitive the first go around. If you think I'm being absurd, okay, but like it's going to happen, and it would be a lot less likely to happen if we leverage an existing construct that is more generally understood.

aadamsx commented 2 years ago

You’re still making the assumption that it is desirable to call anything and everything from pipes, where I think I made it abundantly clear that is not a desire in the first place.

Right, it's not like when they added OOP syntax sugar to JS they expected everyone to wrap all their existing code in Classes or everyone to use Classes for all new code. It's an option if you want to write that way. They didn't pull back from OOP because devs would have to write code differently to interface with it.

Same goes for adding FP syntax sugar, why do we step back from true FP principles like function composition and currying because someone might have to call a curried function or wrap their code? If you don't want to use the features -- you don't have to! But don't kneecap the language with Hack because to use it you have to write new/different code! This argument doesn't make much sense.

mAAdhaTTah commented 2 years ago

You’re still making the assumption that it is desirable to call anything and everything from pipes, where I think I made it abundantly clear that is not a desire in the first place.

I'm not. I am arguing that the universe of things you can put in a Hack pipe is far greater than the universe of things you can put in an F# pipe. I would by extension argue that the universe of things that benefit from the Hack pipe is far great than F# pipe.

What you’re suggesting is a new syntax for any call expression in the language, regardless of asking the question whether we should want that.

I'm not sure what you mean by this, especially insofar as x |> f(^) looks familiar to anyone who has written f(x). The whole goal of Hack pipe is to avoid a novel call expression.

“I don't look forward to trying to debug a broken npm package and see it's written with pipes for every function call.”

Amusingly enough, this is how I feel every time I pull up a codebase that uses Ramda! I used that library extensively for like 2 years, it's an absolute nightmare to debug because there's no reasonable place to put breakpoints, and it's impossible to explain to non-functional coders because there are too many concepts to explain. So I don't use it anymore, which is a shame, cuz there are significant benefits to the library but they're impossible to integrate without taking on a lot of cognitive overhead.

With syntax we can put a breakpoint in between individual steps in the pipe, similar to how you can put a breakpoint after the arrow in a one-line arrow function. Including an explicit placeholder means I can hover it like a variable & get its type in VSCode. It's easier to debug when its integrated into the language proper, and we still get many of the benefits of Ramda's pipe / compose.

Let me provide an illustrative example, loosely modeled on an actual example of something I had to do at work. At a high level, I have some user input that I need to combine with some back-end data which I then need to ship off to another API. Using built-in fetch, the code looks something like this:

// Assume this is the body of a function
// `userId` & `input` are provided as parameters
const getRes = await fetch(`${GET_DATA_URL}?user_id=${userId}`)
const body = await getRes.json();
const data = {...body.data, ...input }
const postRes = await fetch(`${POST_DATA_URL}`, {
  method: "POST",
  body: JSON.stringify({ userId, data })
});
const result = await postRes.json();
return result.data

This sort of data fetching, manipulation, & sending (all async) is a very common problem to solve. Any of the fetch calls could be other async work (writing to the file system, asking the user for input from a CLI, requesting bluetooth access (!), interacting with IndexedDB) – all of these APIs are built around promises. Sure, I could linearize it with intermediate variables as I did but these variables are basically useless to me except as inputs to the next step (I actually wrote res as the first variable and had to go back & change it once I got to the second fetch). There was an extensive discussion about this in #200 (see my comment here for another real-world example).

Sure, maybe in the real world, I'd use axios, and maybe I'd wrap up some of these functions up into an API client. Even then, we'd have like:

const fetchedData = await api.getData(userId);
const mergedData = { ...fetchedData, ...input };
const sentData = await api.sendData(userId, mergedData);
return sentData;

Compare all of that to Hack:

// The `fetch` example:
return userId
|> await fetch(`${GET_DATA_URL}?user_id=${^}`)
|> await ^.json();
|> {...^.data, ...input }
|> await fetch(`${POST_DATA_URL}`, {
  method: "POST",
  body: JSON.stringify({ userId, data: ^ })
});
|> await postRes.json();
|> ^.data

// Or the axios example:
return userId
|> await api.getData(^)
|> { ...^, ...input }
|> await api.sendData(userId, ^);

This is significantly clearer to see, step-by-step, what's going on. Admittedly, I could combine steps (maybe the data merging can happen inline with the API call; it would save me from writing mergedData) or use better variable names (the data suffix everywhere annoys me), but at core, this what the pipe operator is for: linearizing a nested sequence of expressions.

There are even like small cases, where I've got a small function call with maybe an expression in it, and then I need to call one other function after it to post-process. I would love to be able to just do |> Object.keys(^) (this is a common one for me) after evaluating that function + expression, rather than having to put Object.keys in front and wrap everything, which now puts the order-of-operations in the wrong direction – Object.keys reads first, but evaluates last. I'm not intending to use it everywhere, but this one-off case is more readable than the base case because the code now evaluates in the same order it's read.

mAAdhaTTah commented 2 years ago

I guarantee you when this gets released we will see ^.lets = 'hooray' on stack overflow with placeholders, and we won't with lambdas, because lambdas have been in the language for several years now, and function expressions several years before that.

Oh yeah, you'd never see that with arrow functions.

runarberg commented 2 years ago

@mAAdhaTTah It is already easy to linearize your code example using .then():

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

In fact I would probably write a utility function here: getResponseData.


async function getResponseData(response) {
  const { data } = await response.json();

  return data;
}

return fetch(`${GET_DATA_URL}?user_id=${userId}`)
  .then(getResponseData)
  .then((data) => fetch(`${POST_DATA_URL}`, {
    method: "POST",
    body: JSON.stringify({
      userId,
      data: { ...data, ...input },
    }),
  }))
  .then(getResponseData);
voronoipotato commented 2 years ago

@mAAdhaTTah at least you can google that. Placeholder now creates a context where all these questions need to be asked, again, because they could have new answers (regardless of whether they do). It's part of what I'm going through with this proposal, and it's part of why I don't support it.

Personally I find the ^'s everywhere just visually noise, I've mostly abstained from bringing this up since it's like almost entirely taste, but it's an opinion I've seen a lot and seems somewhat on topic for #225. I genuinely had to stare for a while at ...^.data and maybe I'm dumb as a bag of bricks, but I'm probably not the dumbest. To be clear I don't think other placeholder values that have been discussed are particularly more readable, they're just different shaped line noise to me. The most readable may have been ## because at least it is a little harder to overlook. I personally would discourage inline code and encourage named functions. I think lambdas are more okay because they're easier to refactor to named functions and they have variable names baked in. In this way hack pipes work directly against my personal tastes because the easiest thing to do is to put code after the |> and nothing is named.

On the point of breakpoints, there's no rule saying you can't put breakpoints with a pipe operator that takes functions. If I were implementing it, I would.

edit: wow just saw runarberg's translation to then and that's a lot easier for me to read and broadly a lot more descriptive.

ken-okabe commented 2 years ago

Functional Programmers worry about mathematical consistency: g -> f is the same as f(g).

  • In F#, this looks like: g |> f
  • In Hack, it's g |> f(^)

You really don't have to be on one side or the other to appreciate that Hack robs the JavaScript FP community of a certain kind of transferable mathematical thinking. "Readability" to them means something very different to "avoiding temporary variables". F# affords another kind of functional compositional to JavaScript.

Thank you for mentioning that @shuckster, and actually, my concern is not only "Readability" in the mathematical sense but also with Hack pipe, we are no longer be able to write a mathematically consistent code because it has broken the law of Algebra.

Please refer to Concern on Hack pipes on algebraic structure in JavaScript My https://github.com/tc39/proposal-pipeline-operator/issues/223#issuecomment-923369555

voronoipotato commented 2 years ago

The thing is, there aren't fewer temporary variables in hack style pipes, they're just all labeled ^. If we want to pipe to a lambda, so we can have a variable name with placeholder pipes, this is what we'll have to do.

var x = 10
var y = x |> (goodName => goodName * 12)(^)
//instead of
var y = x |> goodname => goodName * 12

I don't think ^ for all intermediate variable names is very readable at all. ^ may be readable in this trivial case, but we're under the understanding that this would benefit from a name, otherwise we wouldn't have variable names in javascript, we'd just have $1, $2, $3 etc... So then they say "well you can create a function" okay let's compare that case...

var x = 10
var y = x |> goodFunction(^)
//instead of 
var y = x |> goodFunction

So if you like names for intermediate variables, placeholder pipes aren't great, regardless of your views of functional programming. They are in my opinion, more verbose in the cases where you name things, and more difficult to read when you don't. Placeholders sound nice with multicase functions, but I think wrapping it in a unary function is vastly more readable because it's actually named. I find ^ hard to visually tease out and difficult to predict what it was supposed to be, for example f(x,z,^,y,a), you literally can't tell what ^ should be without looking at the function definition anyway. I feel like placeholder pipes avoid work to make things more murky, which really is the opposite of what I would think most people want. People complain all the time about bad variable names. I don't like the idea of having the equivalent of 20 functions or "expressions" all with the variable named ^ and I genuinely think that it's going to bite all of us later on when we're wondering what ^ means in a given pipe.

ken-okabe commented 2 years ago

https://github.com/tc39/proposal-pipeline-operator/issues/225#issuecomment-922969225

My general perspective is forcing mainstream JS to adapt to this API will be more painful than asking functional JS to adapt to Hack.

As several others indicated including the issue owner, the readability does not come from Imperative Programming.

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

The mainstream has been the OOP or Imperative Programming, as a result, we have been forced to write Math.pow(Math.pow(2, 3), 5).

Having obtained the new binary-operator **, I think the mainstreaming are going to use 2 ** 3 ** 5 because this is much easier to read and understand.

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

asking functional JS to adapt to Hack

In principle, functional JS will never adapt to Hack because in my perspective, this hack-"operator" is the first binary-operator that has broken the basic rule of Algebra in JavaScript.

This "operator" is something between an expression of the existing algebraic binary operator of operation of function application and a newly invented statement by human who are free from mathematics and play and tweak mostly because they need to fit to another artificially designed statement, according to the Brief history of the JavaScript pipe operator , especially async & await that is so unfortunate.

So, it's highly possible that FP community will ignore hack-pipe.

For mainstream, they have new context variables in their hand every time they use the new hack-operator although FP community won't touch such a thing from the beginning, the mainstream will touch that, and I don't think the majority can avoid the complexity of context variables.

Finally, hack operator will produce many syntax-errrors because in design, again, this does not hold associative law, so some ( ) in the context of "operator" itself does not even has meanings and surely not replaceable to others,

https://github.com/tc39/proposal-pipeline-operator/issues/223#issuecomment-923369555

(foo(1, ^) |> bar(2, ^)) is not even leagal synatax on its own.

In mathematics, there is no such situation (b * c) is not even legal syntax on its own where a * (b * c) is expressed. The huge disadvantage of loss of robust principle Reverential transparency is also inevitable for the mainstream. I feel sorry for them.

ljharb commented 2 years ago

so you're saying x - y is the same as y - x? or x in y is the same as y in x? I'm really not sure where you're getting the idea that "binary operator" guarantees any of the things you're suggesting it does.

ken-okabe commented 2 years ago

Yeah, they are not associative and not monoid. In contract as you may have seen https://github.com/tc39/proposal-pipeline-operator/issues/223#issuecomment-922841935

image

g(f(x)) === x |> f |> g is associative and monoid.

and no one here told you every "binary operator" is associative. What I told you is a binary operator of function application that is associative in the definition becomes no longer associative, and Algebraic structure is spoiled.

Do I make myself clear?

ljharb commented 2 years ago

Right, so it's perfectly fine that there's a binary operator that's not associative. So why is it so tragic that pipeline isn't?

ken-okabe commented 2 years ago

it's perfectly fine that there's a binary operator that's not associative.

Yeah, it's perfectly fine. You are right.

What I told you is a binary operator of function application that is associative in the definition becomes no longer associative, and Algebraic structure is spoiled.

This is not fine and very tragic. Do I make myself clear?

ljharb commented 2 years ago

It's not a function composition operator though, and never has been. Maybe that's the disconnect?

ken-okabe commented 2 years ago

It's not a function composition operator though, and never has been. Maybe that's the disconnect?

It's a typo and I fixed it.

What I told you is a binary operator of function application that is associative in the definition becomes no longer associative, and Algebraic structure is spoiled.

This is not fine and very tragic. Do I make myself clear?

ljharb commented 2 years ago

No, because function application can't possibly be associative in the way you describe, in JS, so it seems like this is a category error.

ken-okabe commented 2 years ago

You don't understsand, for associativity, It's the identical meaning in algebra and just layer is in Monoid (S = S S) or Monad (M = M F) .

https://github.com/tc39/proposal-pipeline-operator/issues/223#issuecomment-923122982 I'm sorry it seems you have not studied the Haskell page I linked: https://wiki.haskell.org/Monad_laws image

For your convenience, I would rewrite to Associativity: (m |> g ) |> h === m |> (x => g(x) |> h) or Associativity: (m |> g ) |> h === m |> (x => x |> g |> h) as (x => x |> g |> h) is the function composition of g and h Associativity: (m |> g ) |> h === m |> (g . h)

function application IS associative in my describe.

Do I make myself clear?

ljharb commented 2 years ago

JS isn't algebra. Saying "something is math" doesn't explain why it's necessary or important.

Function application in javascript is not associative, because f(g(x)) and g(f(x)) have precisely zero guarantees that they do the same things. Obviously you could write an f and g where that holds, but the only thing that matters is that you can write them where it does not hold.

ken-okabe commented 2 years ago

So do you accept

What I told you is a binary operator of function application that is associative in the definition becomes no longer associative, and Algebraic structure is spoiled.

? Then accepting the fact I told you, now you are opening new topic?

JS isn't algebra. Saying "something is math" doesn't explain why it's necessary or important.

Well, then again I must repeat:

In mathematics, there is no such situation (b * c) is not even legal syntax on its own where a * (b * c) is expressed. The huge disadvantage of loss of robust principle Reverential transparency is also inevitable for the mainstream. I feel sorry for them.

ljharb commented 2 years ago

I'm still not understanding. It seems like you have some assumptions that aren't shared - for one, that it makes remotely any difference whatsoever if a binary operator is associative; that algebraic laws matter even a little.

At the risk of inciting debate, programming isn't mathematics. Math education is not required to program, and mathematical laws do not constrain programming. You seem to be presuming that they do - but that's not the case.

ken-okabe commented 2 years ago

Function application in javascript is not associative, because f(g(x)) and g(f(x)) have precisely zero guarantees that they do the same things.

It's a Commutative law, and function application generally does not have property of Commutative and that is the general fact in algebra, not only in JS.

No one discuss on Commutative law, and Associative law is spoiled that I told you. Ok?

ljharb commented 2 years ago

No, I still don't understand. Maybe assume I have no college degree and no formal math education, and maybe assume that if a concept requires those things, then it's not necessarily a good concept to apply to JS.

ken-okabe commented 2 years ago

Assumption of degree level of a programmer is nothing to with binary operator implementation to a programming.

Are you insisting if a programmer only has a knowledge of elementary school. the rule of algebra could be ignored? Do I understand you correctly?

ljharb commented 2 years ago

I have a knowledge that far exceeds "elementary school" and yet isn't "formal math education". I'm saying that if you want to explain your argument successfully, you need to be capable of explaining it to someone whose education stopped after high school (after age 18, since "high school" may be a colloquial American term).

ken-okabe commented 2 years ago

So I assume you basically insist that language scheme of JavaScirpt should be at most for someone whose education stopped after high school, and since they don't care, the higher math operator's claim does mean nothing in general. Do I understand you correctly?

Never the less of whatever educational level of programmers, the form of f(x) === x |> f is monad. |> has a monadic structure that is the fact, and the function application is not a special rare operation at all. It is Not promise functor.

Spoiling the math element does have effects on things even you don't care now. Still you don't care?

ken-okabe commented 2 years ago

Would you mind explaining it as if we were 5 years old? What's the concrete point you are trying to make?

Since it is elementary school, we should be able to understand your explanation. Otherwise, I will put the ownership on you as the teacher here.

I respect these inquiry from all of the members here. I appreciate and I will. Just give me a time like 24 hours as I have stuff to do like anyone else. Thanks.

ken-okabe commented 2 years ago

Also @stken2050 I am not appreciating your condescending way to express yourself right now, you are one of the few people that have no activity in GitHub,

Eah, I'm sorry for your misunderstanding, I have other accounts for other projects that have been very active.

hijack the conversations around the operator forcing everything else to deal with it, we like it or not.

This is a new concept to me, and please stop personal attack that is against code of conduct here. Be careful.

I think the TC39 are really smart people that they know what they are doing; and assuming that such people are not capable of making decisions taking into consideration what you explained, it is a bit far off.

I do not appeciate Authoritarianism and you speak up without any evidence.

Maybe it is a matter of a combination of how you express yourself thru language and patience. I would appreciate it if you are a bit more friendly communicating.

Sure that is why I told you

I respect these inquiry from all of the members here. I appreciate and I will. Just give me a time like 24 hours as I have stuff to do like anyone else. Thanks.

and your reply is

Also @stken2050 I am not appreciating your condescending way to express yourself right now, you are one of the few people that have no activity in GitHub, and hijack the conversations around the operator forcing everything else to deal with it, we like it or not.

and personal attacks. Thanks.