-
Notifications
You must be signed in to change notification settings - Fork 110
Could |>
be amended with a ...
spread?
#275
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
Comments
That seems like it would require forcing a choice of unary functions, which would mean that the array would be likely to contain new wrapper functions whenever more than one argument was provided, which was a major sticking point for implementations who want to avoid encouraging excessive creation of new functions. |
I don't think developers, especially the FP inclined, care whatsoever that a JS engine wants to "discourage" creating wrapper/adapter functions. The design philosophies behind FP style code have pretty much always been more important to the FP developer than the capabilities of the environment that's running the code. IOW, FP developers are going to compose functions -- whether they be natively unary or adapted via wrapper functions, currying/partial-application, etc -- regardless of what syntax is or is not present in the language. Trying to discourage FP patterns because JS engines find them hard to optimize seems like a flimsy argument against providing affordances to developers who are already doing it the harder/uglier way, and will just keep doing so unless/until the language offers them a nicer path. |
While i agree with you, it’s nonetheless an intractable obstacle. |
When I presented proposal-function-pipe-flow, my argument had four thrusts:
The objections that I got from the Committee were twofold. One objection was that the pipe operator may address the static-series use case well enough. The second objection was that both the static-series use case and the dynamic-series use case are easily addressed with a userland function. Actually, the second “just make a userland function” objection was arguably more strongly made than the “just use the pipe operator” objection. I should change my summaries to emphasize this. With that said, I think the function pipe (input, ...fnArray) { // Userland function.
return fnArray.reduce((value, fn) => fn(value), input);
}
const compSteps = [ fn1, fn2 ];
if (whatever) compSteps.push(fn3);
"Hello" |> pipe(^^, ...compSteps) |> console.log(^^); …except that |
There's absolutely no advantage to that vs: pipe( "Hello", ...compSteps, console.log ); But comparing to: "Hello" |> ...compSteps |> console.log(^^) I can certainly see that as being a reasonable evolution to consider, especially since it could mean ditching part or all of a userland lib, and maybe even getting (eventually) some nice perf optimizations from the declarative syntax. So what it keeps coming back to is, of course there are ways (in userland) to do this stuff... we've been doing it for 15+ years. But if JS is contemplating a new syntax/operator that is ostensibly fairly closely related to stuff devs have been solving with userland functions, and they are hoping that at least some of that userland usage shifts over to the native syntax..... in that case, it seems like a small affordance to add Why wouldn't that be a sensible thing to consider? If TC39 isn't hoping/contemplating that a good chunk of userland composition will move over to |
I can’t speak for anyone else, but i don’t think it does - i think strict FP/fantasyland usage is the very narrow slice. Whether it should be is a very different discussion, but doesn’t change the reality. The vast majority of my composition uses are not strictly unary functions, and the current proposal is a much better way to express that composition than current options for me. |
It may very well be that non-canonically-FP styles of composition, which are looser and more imperative/pragmatic, are in fact the wide majority of "composition" in the wider JS landscape. I'll certainly concede that. But... most of the people who do things like Unfortunately, they're not likely to be part of the early- (or even mid-) adopter crowd, since they don't know the terms and this topic is probably not on their radar screen at all. The FP-focused crowd is absolutely aware of the topic, and likely aware of the proposal (to some degree) and is avidly watching to see how it all shakes out. They're the ones who are going to either use, or not use, this new operator predominantly. So I would have thought that factoring in their needs and desired affordances might have had a higher priority even if the count of lines of code potentially affected is absolutely smaller given that it's a more interested/vocal minority that's tracking this topic. |
It’s certainly true that The purpose of the proposal is explained in the explainer, although perhaps it is too wordy. Basically the intent is that: “The ES pipe operator is a zero-cost abstraction for flattening deeply nested expressions, including but not limited to n-ary function calls, array/object literals, and A pipe-function API could also flatten deeply nested expressions, but it has a runtime callback-allocation cost for anything other than already-unary function calls, which several engine implementers do not want to encourage especially in hot paths. (A pipe-function API also may be clunkier for many types of expressions, such as array/object literals and template literals.) #273 already discusses this; feel free to comment there regarding “why an operator instead of a function”. In particular, take a look at the real-world React example in #273 (comment). The explainer already has this example and many more real-world examples from various codebases, but it still needs to contrast it with the pipe-function-only approach. |
IMO, they're not "roughly equivalent" or even close. the I'm not here asking for the function form (that horse was already beaten... to death apparently). I don't even prefer to keep using my userland function approach. I'd like to be one of the folks (hopefully many) who can see myself converting to using So I'm asking to consider an affordance that actually makes Without something like But I'd like to be a |
BTW, someone else asked me for more real-code examples of dynamic composition, of the form where File (2) is how I currently do things, file (4) is what it would look like trying to use Stretching my dream wishlist even further, file (6) shows a |
Sure, when the entire meat of the expression is already in the
The issue here is that this isn't a pipeline-specific proposal. You're asking for a function-composition operator (an n-ary one that works over a list), and suggesting a syntax based on spreading a list into the arglist of
|
I don't resonate with that perspective at all. From where I sit, So I don't see it as so outlandish to suggest a composition-related feature/affordance to be tacked onto the single syntax operator that does composition. I'm definitely not advocating for "general function composition" everywhere... I am only asking if we could make |
I don't see how function composition is useful for piping, but not useful to produce, say, a |
FWIW, I don't think the example in your Gist actually requires the feature you're asking for. You don't need this spread syntax to choose which function to call. You can just use a ternary inline. Typing from my phone, so apologies if the syntax isn't quote right, but I think you can just do: record |> ^.isEmployee ? getEmployeeName(^) : getCustomerName(^) No? |
I never implied "requires"... the examples were to illustrate an ergonomic advantage (namely, syntax in place of a userland lib). In particular, if The spirit of asking for |
To me, their names imply they’re callable, since they’re verbs, and singular - which may be the source of confusion. |
@ljharb -- I don't think it's productive to be bikeshedding about variable names here, as that's obviously missing the point. But... in the first example (2) they were functions, and I kept them the same name when I changed them to arrays so that the subsequent example had consistency with the previous. Moreover, in files (4), (5), and (6), the very first line of each snippet, where the variables are being declared, has a clear code comment indicating they're arrays, contrasted with the code comment in (2) that says they're functions. |
It's an ergonomic advantage specifically for the case where you build up a composition in an array and spread it lazily into |
That's only one possible incarnation of "dynamic composition" (meaning dynamically determining the steps in a composition). I used the array approach because it happens to be something a theoretical Functions composed via partial-application of But I wasn't proposing that some form/extension of It may seem idiosyncratic to dynamically construct compositions, but I didn't invent the idea -- saw it years ago -- I've simply been a fan of and using it for many years. I've also taught dynamic composition as part of my Functional-Light JavaScript book and the corresponding course, so many thousands of other JS devs have at least learned it from me. I can't say how popularly used it is, but I use it quite often.
Again, I don't know why we're bikeshedding on how you didn't like the name I picked for the variable. In (2), The rest of the composition does more than simply "get the customer name", because it also formats it. So if I were to assign a variable to the fully completed composition after those formatting functions were applied, I might call it In (4), (5), and (6), the same specialization progression from "get the customer name" to "get the customer name and then format it" happens, but it happens via array concatenation rather than through partial application. But it's the same concept in both cases. Whatever the variable is called, I wouldn't expect so much trouble in recognizing what it's holding: a partially-applied function, and that the rest of the arguments this function is expecting are further functions to participate in the composition, because I partially applied Perhaps I should have used TypeScript to make the types of the values more clear? Or maybe I should have just stuck to the tried and true |
I was puzzled about that line of reasoning when we have the group proposal at Stage 3 that could also practically fit into a single line of userland codegolf: const byGroup = fn => [(acc, item) => ((acc[fn(item)] ??= []).push(item), acc), {}];
const arr = [
{ id: 1, category: "category 1" },
{ id: 2, name: "category 2" },
{ id: 3, name: "category 1" },
{ id: 4, name: "category 3" },
];
arr.reduce(...byGroup(x => x.name || x.category)); Slightly ridiculous, but real prior art is barely over 10 lines. Perhaps Apologies if this is slightly off-topic, but since the opening of the issue by @getify seemed to have been prompted by the rejection of the |
Correct, there is a tradeoff between "difficulty of implement in userland" and "expected utility"; if one is higher the other doesn't need to be as high. (But ideally we mostly focus on things that are high in both areas, since our time is finite.) That said, please take discussion of the |
@tabatkins in all fairness, @js-choi brought up the |
Yeah, mentioning it obviously isn't off-topic - it's clearly a related subject - but prolonged discussion about its personal merits and why it was or wasn't adopted by the committee is. |
@tabatkins I agree there's a line. Not sure exactly where it is. But more to @shuckster's point, there's a line, a minimum standard, the committee feels If we're postulating that |
That's pretty straightforward. Returning to the subject of the thread, tho, the essential objection I still have is that you were able to use I believe all the examples you gave would be significantly improved in readability if they were actually functions, composed piece-by-piece if necessary, rather than arrays of functions carried around and only at the end spread into pipe(). (This is strongly influenced by the names you chose - giving the arrays names as if they were functions, rather than more idiomatic "array-like" names - which suggest they should be callable, when in fact they are only usable in a very specific location and manner.) There's some perf overhead to that, of course, but whether that overhead is significant or not depends heavily on the precise usage. |
That may be your perspective, but I don't think it comports with the broader FP thinking, the dao of FP as it were. An array of (unary) functions that will later (lazily) be composed, is isomorphic to an eagerly composed singular function. They're isomorphic because you could, with the right tools and forethought, design a transform to go from one to the other, or vice versa. In FP, you choose to use one representation, or another isomorphic one, depending on whichever one is most convenient for the task at hand. An array of functions happens to be a convenient way of dynamically constructing a composition. It's not the only representation, but I think it's arguably the most convenient way available to a JS program. When you're dealing with isomorphisms, you don't typically concern yourself so much with the naming of something. In fact, in FP, you rarely name things, preferring instead to have expressions flowing into other expressions. Maybe it's because FP developers are tired of arguing over names, I'm not sure. In any case, I only named stuff because I was trying to not mangle underlying concepts under one giant expression of a program. But clearly, we keep coming back to an objection over naming, as if the name of something belies its fundamental design flaws. Sigh. I definitely regret ever posting any concrete example code here. I probably should have known that whatever that code included would be bikeshed to death, instead of engaging in substantive discussion on the underlying concept (dynamic composition). You can continue to discount dynamic composition as some novel artifice that I came up with, but I again will assert, I learned it from other FP programmers, and I've subsequently (to this thread) re-verified with more than one of them that it's not some unique invention of mine, but a fairly reasonable application of isomorphism. Dynamic composition far predates any meager attempts I've made in recent years at learning FP, and indeed exists wholly independent of whether I'm a good messenger of its concept or not. Some FP folks (like me) use it regularly, and at least one person (me) would like it be a bit more ergonomic in JS. But the nature of the resistance seems to be that -- as has happened a number of times over the years with several other proposals advanced by FP leaning developers -- since it doesn't look like the mainstream JS you're used to seeing, it's somewhat summarily discounted. That shouldn't be so surprising, I guess, but it's nonetheless a disappointment. I maintain, however, that the primary reason such a style or code technique is not as popular, is not because it's not useful or interesting, but because it's currently not very ergonomic, so most people end up solving such problems in other (more imperative) ways. If it were made to be more ergonomic, I believe more people would take advantage of it. |
Since the resistance is clear and unwavering here, I'm resigned to dropping the subject. But as that means there's nothing left to lose, I'm going to point out one more fact which I almost brought up in my OP, but instead I withheld up until now, fearing that it would both cloud the discussion and, likely, prove a sufficient deathblow in and of itself. In the deeply divided contest over Hack vs F# style pipe operator, the main proponents of F# were those who do most (or all) of their compositions with unary functions. They were, in the end, told that their style of coding was idiosyncratic or non-mainstream enough -- or, strangely, "too hard for JS engines to optimize" -- and thus Hack-style emerged as the victor. There's still plenty of bitter resentment lingering, however, as it was a tough pill for some to swallow. But what if that divide could have been healed (mostly)!?!? The // (1) F# style pipe composition:
val |> something |> another(10) |> whatever
// (2) Hack + F#:
val |> ...[ something, another(10), whatever ]
// (3) instead of:
val |> something(^) |> another(10)(^) |> whatever(^) Of course, (2) would only be a small (but meaningful) consolation prize to the F# proponents. That said, my guess is, if they can't have (1), they'd rather have (2) than (3). |
There’s certainly an ecosystem divide between unary-functional programming and other styles with n-ary function calls and other operations – where the latter includes a large portion of old and new web APIs and other popular APIs. There is also a divide between those other styles and method calls. My hope remains that, even if unary FP remains at its status quo for now, and even if '--' + input
|> document.getElementById(^^)
|> asyncParallelFind(^^, { timeout })
|> await Promise.all([ ...^^, asyncRunAnotherTask() ])
// pipe is a one-line userland function, maybe someday standardized:
|> pipe(^^, ...fnArray)
// This line is inconvenient to put in pipe’s fnArray because of
// its complex argument; it is easier with |>:
|> console.error(`Error: ${^^} is invalid.`); Here, the Having said that, if |
@js-choi I noticed your topic-token is
I checked the README.md and notice it hasn't been updated to reflect this preference, which was apparently agreed to nearly a year ago. I know we're all very busy here so this isn't a berating. But regardless of how anyone is falling on @getify's previous example does look different: // (1) F# style pipe composition:
val |> something |> another(10) |> whatever
// (2) Hack + F#:
val |> ...[ something, another(10), whatever ]
// (3) instead of:
val |> something(^^) |> another(10)(^^) |> whatever(^^) Again, apologies once again to @tabatkins for being off-topic, but if the the topic itself is off I figured it was worth bringing into the topic. 😁 |
FWIW, I don't personally see myself using For example, Even with the knowledge that unary function composition patterns are not as eagerly optimized by JS engines, and so I may be paying a perf penalty in doing so, FP patterns -- and especially, lots of familiarity with those existing patterns -- would weigh much more heavily in my mind to keeping the status quo, instead of being an encouragement to start throwing The real attractiveness of |
Quick response on this, because I've seen it brought up in this dismissive way before: The objection was not at all that "unary functions are hard to optimize". That would be a ridiculous statement to make on its face. It's that pipe() (and the F# pipe operator) encourages the use of single-shot dynamically created functions (both arrow functions, and the return values of higher-order functions like The browser engineers who leveled this objection are neither stupid nor capricious. Please respect the reasoning of people on the opposite side of the argument from you rather than dismissing them offhand as "strange" and presumably erroneous. |
@shuckster: As you say, this is a bit off topic, but I can quickly address this. We haven’t updated the explainer and spec yet to
@getify: Yes, as you say: in the near term, the use cases of I attempted to advance proposal-function-pipe-flow as a stopgap solution for unary-FP-predominant styles, and I apologize that I was not able to advance it at this time. But, although the Committee considered and rejected unary-FP affordances in the short term, it may reconsider them in over the long term. We can only see—in the meantime, the userland status quo for unary-FP-predominant code will continue on. |
Back on the topic more directly:
This suggestion does not make this coding pattern more ergonomic in a significant way. The existence of an isomorphism between an array of functions and a composed function doesn't make the two equivalent in usability; you still can't do I say this regularly, but I'll remind again: I am also an FP proponent. I grew up on Common Lisp and made my own bug-ridden informally-specified version of half of Haskell's Arrow type in it. I've done my time in the point-free mines, happily eating my curry. I still think JS would benefit from formalizing monads. And it is precisely because I've gotten so deep in the guts of this topic that I'm so wary of trying to add too much of it to JS. The affordances just aren't here for it; the syntax itself fights you if you go too hard in certain directions. The features themselves sometimes scare me as well; I don't have a single piece of heavy point-free programming that I can look back at and say "ah yes, this is readable and easy to understand". I regret every So I understand where you're coming from, and have many of the same interests. I'm not an imperative programming partisan here to ruin your day. I just disagree with you. |
Indeed, I have always found it useful to distinguish unary FP from n-ary FP (see #233 (comment)). Unary FP by necessity heavily involves currying, and languages based on unary FP – like Haskell – typically have “auto-currying” function syntaxes. In contrast, n-ary FP involves currying much less often, and languages based on n-ary FP – like Lisps – typically do not have auto-currying syntaxes. (Indeed, Lisps are based on lists as much as they are based on lambdas; they are fundamentally n-ary.) Because of this, Lisps and other such n-ary FP languages typically focus on “high-level” functional combinators like monadic binding – and less so on “low-level” partial application, currying, and unary functional composition. Of course, JavaScript is an n-ary FP language, and, in this manner, it is more similar to Lisps than to Haskell; although Lisps and JavaScripts both can accommodate unary FP styles, both arguably lend themselves more readily to n-ary FP styles.
|
I find this assertion the most difficult thing to understand/swallow in this whole thread. It seems outlandish to me that JS added the If JS engines were worried about having inline So I give precisely zero credence to a claim now, years after The general crowd of JS developer has already loudly voted, and they said they like the ergonomics of I'll also add that my lack of respect for this claim -- I'm not saying anything about the people claiming it, only the claim itself -- comes from observing that over the years, JS engines (and the people writing them) have shown unbelievable amounts of talent and creativity in creating a whole universe worth of JS engine optimizations that, quite literally, CS professors used to teach students (like me) were never going to be possible. I have trouble believing there's anything that JS engines cannot optimize, given how much magic they've already demonstrated and proven. Moreover, a lot of that effort seems to have been poured into increasing the surface area and complexity/capability of From where I sit, my opinion is, the question is not, "can a JS engine optimized for __", but rather, "is there sufficient motivation for the engine to optimize for __". It's not because FP is fundamentallly less optimizable than class-orientation -- I just don't buy that -- but because, as TC39 decisions have borne out year after year, there's more appetite/motivation to put effort into designing, and optimizing, class-related features. |
The intent of this thread was to push back on the edges of that "decision" for some reconsideration. It seems from the tone of response here that the thread has, minimally, accomplished its goal, in that it has elicited a pretty clear "nope we're not ready to look at this further" response. I dunno how representative of broader TC39 this thread's discussion is, but since I'm not on TC39, it's the best forum I have. And I think it's clear what the outcome is. Please feel free to close the issue and let's just move on. |
Understood—my apologies again that, at this time, we’re not able to accommodate your use cases as a language feature. Hopefully, we have the chance to revisit this again in the future. |
Just to close up the conversation:
Yes, arrow functions are completely ubiquitous in JS and wonderful. But note that what people don't commonly use arrow functions for today - organizing individual steps of operations. People don't write code like: for(let val of vals) {
val = (x=>x + 1)(val);
} because doing so is pretty obviously ridiculous. The perf argument against this kind of code pattern isn't even relevant, since it's completely unergonomic and unattractive to start with, so nobody is going to write it. But that is precisely the code pattern that pipe() and F#-style pipeline embody with You're right that, given sufficient motivation, we can probably usually optimize away most of the cost. But it's better, if possible, to avoid introducing the cost in the first place. The current pipeline operator is approximately a zero-cost abstraction here, which is nice from this perspective. |
Given that the
pipe(..)
/flow(..)
proposal failed to gain stage-1 status and was subsequently revoked, and the main justification for that was because it was felt that we should just throw all our eggs in the|>
basket... I am here to ask for considering an addition to|>
that would help address some of the use-cases I was advocating for in thepipe(..)
/flow(..)
proposal, which seem to have been glossed over or discounted.I'd like to ask that
|>
be extended with a...
syntax variant (similar to function arg spread):Put simply,
...compSteps
in a step of a|>
pipeline would be spreading out all the contents of thatcompSteps
array/iterable as individual|> fn(^)
expression pieces. It would basically be like doing:By having a
...
option with|>
, this operator can be bent to some of the "dynamic composition" use-cases that it currently cannot really serve. The rejected/deferredpipe(..)
andflow(..)
would have served them more directly, IMO, but since those have been shelved, I'm hoping maybe|>
+...
is the next best option.+@js-choi
The text was updated successfully, but these errors were encountered: