-
Notifications
You must be signed in to change notification settings - Fork 107
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
Consider Elixir-style pipeline as one of the blessed alternatives #143
Comments
Related to #20. I suspect whomever wanted to implement this in babel could piggy-back off the F# parsing and just write an additional babel transform. |
Right, I see that there were reasons for not going with #20, but it keeps coming back. Piggy-backing off of F# for the implementation sounds good. I think it'd also be helpful to have an explainer document and maybe a draft specification to document it and make it more concrete. There are some edge cases to think through, e.g., should it be inserted into the argument list if there are parentheses around the thing on the right hand side of the |>. If anyone wants to take this on, please say so here and I'm happy to help you get started. |
I’d be happy to draft an explainer and specification for a tacit-first-argument-only pipe operator later, although for now I’ll be currently working on the Babel transform for the smart-pipeline proposal. We also will need a name for the new proposal (see #128). I suppose Elixir style might do... |
Let's go with Elixir for the name--we've been using it in this repo, and there's a lot of overlap between Elixir and JS programmers, so it could be a useful reference. |
This is going to be the troublesome part, and it's called out specifically in the Elixer docs. We might also want to consider whether the old bind proposal can be updated to insert the LHS as the first argument instead of the |
Indeed, accommodating both Elixir’s first-argument style in addition to last-argument styles without too much magic is one of the goals of the current smart-pipe proposal—although, of course, that other proposal currently makes other trade offs in exchange for such an advantage. In regard to “I keep meeting developers who have the intuition that pipeline should be Elixir-style, inserting the argument at the beginning”, it’d be good and helpful to hear from some of those developers or to see some concrete examples of the code they specifically have in mind. |
My impression so far is pretty simple, roughly: "this is all solved in [xyz language] and it's not so complex; I don't understand what you're worried about". It would be good to discuss more, though, I agree. |
To summarize the biggest downside to Elixir-style: function programming enthusiasts were concerned that they would not be able to use curried functions within a pipeline. For example: var add = x => y => x + y;
var result = 10 |> add(20);
// Elixir-style would translate the above to:
var result = add(10,20);
// which does not work with the way `add` is written. Someone proposed (I forget who) a syntactic solution using parenthesis: var result = 10 |> (add(20)); //=> 30 But many were not happy with it. Personally I think it's a good compromise, and even makes sense: the standard behavior of parethesis is to evaluate the inside before the outside. |
If I’m understanding this correctly, then, the Elixir style for which these people have been asking would interpret function/method-call syntax as a special case: But if I’m understanding it correctly, then that runs into ambiguity problems—the same ones that made me propose restricting smart pipelines’ tacit syntax and forcing everything else to be explicit with the placeholder. For instance, with the Elixir pipe, what would |
OK, thanks for this summary, I will try to circle back with advocates of this variant and see their thoughts. |
As an Elixir developer, it was my assumption that the style was going to be "argument first" (as described by @gilbert and @js-choi above). Per the question above, I think it would look this this, no? // x |> f(a)(b)
function f(x, a){
function(b) {
...
}
} Personally, my favorite part of pipelines are their simplicity, and would be fine if "advanced" syntax just wasn't supported as part of the pipeline (you can always put it into one of the functions). In-fact, IMO, I see these advanced syntax situations as almost oxymoron to using pipelines. Looking forward to the day that I get to write this in JS: viewer =
request
|> accepts(['json'])
|> verify_header(claims, { realm: 'Bearer' })
|> ensure_authenticated()
|> verify_not_blacklisted()
|> load_resource() |
I find the Elixir-style pipe un-JavaScript-y. I can't speak to the intuitions of those who expect that, but I feel like it would be confusing for most JavaScript devs. I'd also be a little concerned about making the communication around the operator more confusing overall if we add another proposal to the mix. I wouldn't want to add this to the mix unless we saw a strong push for this behavior. |
Well, informally, I hear a lot of negativity about smart pipeline, and a lot of people asking for Elixir, but this isn't very scientific. |
To clarify, I think we should consider things very open and unsettled at this point, and consider all three options, if we can manage to come up with some kind of initial guess for what to do about the parentheses semantics. |
I would like to mention just for historical and informative purposes this is closely related to some past discussion (1, 2, 3, 4, 5) in the bind proposal repo. It's also where this grew out of, and I feel it's very much so worth looking through those to pick apart the foundations of this, so we don't forget what led us here and so we don't lose track of the original intent of the proposal. (I feel we've gotten a little too heavily invested in how it interops with mostly pure functional code, at the cost of how it works in the common case of simple pipelining and how it works with procedural code.) |
I agree with @mAAdhaTTah that built in currying is unintuitive for a native javascript operator. Are there any other cases of javascript supporting currying natively? |
A possiblity that combines Elixer-style first-arg passing with the syntactic structure of the bind proposal (as suggested above by @isiahmeadows). Syntax:
Common usage scenario: import { map, filter, collect } from 'iter-funs';
someIterator->map(cb1)->filter(cb2)->collect().forEach(cb3); Pros:
Cons:
Thoughts? |
resolved
They do, it just happens to be what’s used by e.g. underscore. For something existing that won’t match, see current RxJS. |
Let me clarify: first-arg style is well supported by the language outside of any pipeline feature (be it syntax or function). If I create a library I can just choose that style and I know it will work well regardless of whether my users are using pipeline-things or not. Current RxJS, on the other hand, requires use of a "pipe" method (or pipeline operator) to make things ergonomic. |
I wouldn't consider this a significant con considering how few JS devs use C++. You could make a better argument for PHP, since their I would posit the second con, about native support for point-free support, is probably more significant, but I'm not convinced it really carries the benefits the FP community say it does in JS. JS FP works more like Elixir and Clojure than OCaml or F# - arity is a concern, and it's impractical to define any generic auto-currying mechanism because callees can have multiple arities. As for what primitives they have for flipping function application:
I did note when I first proposed it that one of the other major pros (one you didn't list) is that it's already a very prevalent idiom. Lodash, Underscore, and jQuery's array/object utilities all use the first argument for the data itself, so users could start using it right away and we wouldn't need to tell everyone to change all their idioms. |
And another pro with |
Also, just wanted to mention to others here that @zenparsing's idea was something I first suggested here a while back, coming from the context of iterables and generators. |
For more context, I'm not sure the property access confusion concern is C++-specific, as @zenparsing has specifically advocated a pattern like this for private state. |
@isiahmeadows makes a good point about curried vs uncurried languages. I agree that arg-first is already idomatic JavaScript. To add another case study, ReasonML has chosen |
didn't quite understand why -> works and |> don't. |
@mrsufgi Reason actually has both F#-style and Elixir-style forms, but the docs omit this bit:
You'll see a lot of older code using the first, since the second is relatively new. But it's especially notable that Reason specifically added |
@js-choi To be honest, I'm not terribly keen on a "Split Mix"-style operator, as I find similar operators with different semantics too complicated to be worthwhile. I'm not opposed to doing Elixir-style pipelining with a different operator (e.g. |
I'm skeptical of having multiple pipeline-related operators. We should have a pretty good understanding of why we really need both. Less to learn is better, all else being equal. But interesting to hear that other languages went that way. |
I feel Elixir-style pipeline is like the compromise of OO world and FP world, though both world may accept it, both world may also not satisfy with it... |
The elixir pipeline has a very interesting feature in that you don't have to curry your non-unary functions for them to work with it. Indeed it can help replace the current pervasive "chaining" strategies. Currying is cool, but it's not widely used in JavaScript. The "object" of the function being the first argument is. The current proposals which I've been able to find (unary functions only, if you need more arguments you need an inline arrow function or to curry your function) will only cause more boilerplate and confusion. Does this package curry its functions? Did I curry this function? Let me curry this function so people can use pipelines with it. The simple syntax Edit: forgot to mention that clojure's similar So two very functional languages don't force you to curry or change the signature that makes the most sense for regular usage when you just want to pipe several functions. What then is the reasoning for this requirement in this proposal? |
I've read someone concerned with performance. Since this proposal is syntax sugar, your minifier will take care of desugaring it, such that it has less characters and parses faster. Terser will support this for sure, that's a Promise I can certainly keep. |
@fabiosantoscode The performance concerns are typically about the overhead of creating curried functions, not size. |
Got it! Yes, that makes sense. Currying is not free. |
Lemme clarify that for our pipe-first implementation, we just do a syntax transform back to the normal function call. Doesn't disrupt any existing semantics, extremely predictable and toolable, and no runtime performance hit, obviously. |
Using Let's say I want to write a Date library, would I want my function to be of the form |
First-argument injection is, I feel, the least good of all the options presented so far.
|
@tabatkins thank you for your comment. Those are all valid concerns. I wrote down some of my thoughts regarding your comment, but decided to hold it off until I come up with something a little more concrete. |
I agree. My reply is this is probably a case where the concern is not born out in practice. Clojure, Elixir, Racket all implement argument injection as their pipe mechanism, and users of those languages do not find it confusing.
I presume you meant it is a novel mechanism (instead of syntax)? I would say this mechanism is very easy to get used to, as born out in practice in all languages that implement it.
Yes, I agree. That's where Hack proposal would be useful :) My main gist is that argument injection is not a novel idea, and works well in the languages that already implement it. These languages typically pick argument injection as their default pipe mechanism, and argument replacement as the fallback. We shouldn't reject it out right, and it may have some synergy with the Hack proposal. I decided to write a new proposal that combines both argument replacement and argument injection to illustrate my points. Maybe it can alleviate some of the concerns: #84 (comment) |
I'm going to agree with @highmountaintea that this doesn't tend to be an issue when the syntax is clear and unambiguous. This function—argument order inversion can even be seen in non-pipe contexts, for example method calls in Python: class K:
# definition order is: funcName, subject(self), *args(x, y)
def funcName(self, x, y): pass
obj = K()
# call order is: subject, funcName, *args
obj.funcName(x, y) And it doesn't appear to cause trouble once you get used to it.
For the language, yes, but there is precedent in libraries. For example Lodash: // the basic form of lodash functions operate on the first argument:
_.mapValues( _.groupBy(bills, 'type'), g => _.sumBy(g, 'amount') )
// but you can also wrap your input in a proxy and pipe it like this (and since we're talking
// about syntax, let's ignore the semantic difference that this evaluates lazily; what's notable
// is that these methods are not documented individually, they simply defer to the documentation
// for the functions operating on their first argument):
_(bills)
.groupBy('type')
.mapValues(g => _(g).sumBy('amount'))
.value()
// which is very similar to how you would use the regular no-proxy functions with Elixir-style pipeline:
bills
|> _.groupBy('type')
|> _.mapValues(g => g |> _.sumBy('amount'))
To me it feels like the most balanced of all the alternatives presented.
Now why do I think the Elixir-style strikes a good balance between the two? Because F#-style is only really good with unary functions. Elixir-style is good whenever you want to pipe the first argument, i.e. unary functions and then some, and the price you pay over F# for that wider range of easily usable functions is mere Elixir-style balanceWith unary functions - F# wins: input |> func // F#
input |> func() // Elixir
input |> func(#) // Hack
func(input) // desugar Binary with topic first - Elixir wins: input |> (x => func(x, 2)) // F#
input |> func(2) // Elixir
input |> func(#, 2) // Hack
func(input, 2) // desugar Binary with topic second - Hack wins: input |> (x => func(1, x)) // F#
input |> (x => func(1, x)) // Elixir **1 (implicit arrow-function call)
input |> (x => func(1, x))() // Elixir (explicit call)
input |> func(1, #) // Hack
func(1, input) // desugar
Mid-pipeline await: input |> asyfunc |> await |> regfunc // F#
input |> await asyfunc() |> regfunc() // Elixir **2
input |> await asyfunc(#) |> regfunc(#) // Hack
regfunc(await asyfunc(input)) // desugar
Note that unlike special parsing rules needed for await in F#-style, in Hack-style |
For completeness sake, this is what minimal pipelines + partial application proposal + some helpers would look like: input |> func
input |> func(?, 2)
input |> func(1, ?)
const Promise = pipeable(globalThis.Promise) // https://gist.github.com/ducaale/93bd6d49314ef1383f50be95edca9d6e
await (input |> asyfunc |> Promise.then(regfunc) |> Promise.then(regfunc2)) |
That was part of the initial rationale for partial application, to allow both leading-arg (lodash style) and trailing-arg (Ramda style) usages (along with its other capabilities). For me, both F#-style (w/partial application) and Hack-style feel like JavaScript, with semantics that fit with the language. Elixir-style doesn't quite fit with JavaScript semantics, and I'm concerned it would introduce a footgun that is the polar opposite of the confusion around |
This comment has been minimized.
This comment has been minimized.
Another Elixir-style pipeline and capture operator playground I've been experimenting with: engine262 fork |
Closing, since the proposal has advanced with "Hack-style" pipeline syntax. |
@lightmare: For what it’s worth, @tabatkins has actually presented and discussed Elixir style with other delegates: in the 2021-06 incubator meeting. Nobody there seemed to think it was a good fit for JavaScript, though. |
@js-choi as an Elixir developer, I have been following along and have occasionally chimed in. I feel like Elixir's approach would have been an easy fit to JS. You could almost copy Elixir's syntax verbatim. C'est la vie |
I know we previously discussed this at length and settled on F#-style, but I keep meeting developers who have the intuition that pipeline should be Elixir-style, inserting the argument at the beginning. Would anyone be interested in writing this up formally and implementing it in Babel, so we could get hands-on experience with this alternative?
The text was updated successfully, but these errors were encountered: