Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Consider Elixir-style pipeline as one of the blessed alternatives #143

Open
littledan opened this issue Dec 22, 2018 · 34 comments

Comments

@littledan
Copy link
Member

commented Dec 22, 2018

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?

@mAAdhaTTah

This comment has been minimized.

Copy link
Contributor

commented Dec 22, 2018

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.

@littledan

This comment has been minimized.

Copy link
Member Author

commented Dec 22, 2018

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.

@js-choi

This comment has been minimized.

Copy link
Contributor

commented Dec 22, 2018

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...

@littledan

This comment has been minimized.

Copy link
Member Author

commented Dec 22, 2018

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.

@zenparsing

This comment has been minimized.

Copy link
Member

commented Dec 22, 2018

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 |>.

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 this value.

@js-choi

This comment has been minimized.

Copy link
Contributor

commented Dec 22, 2018

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.

@littledan

This comment has been minimized.

Copy link
Member Author

commented Dec 22, 2018

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.

@gilbert

This comment has been minimized.

Copy link
Collaborator

commented Dec 23, 2018

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.

@js-choi

This comment has been minimized.

Copy link
Contributor

commented Dec 23, 2018

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: x |> f(a, b) would be f(x, a, b), x |> o.m(a, b) would be o.m(x, a, b), and x |> anyOtherTypeOfExpression would be anyOtherTypeOfExpression(x). If I’m understanding this right.

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 x |> {}.m(a, b) mean? What would x |> f(a)(b) mean?

@littledan

This comment has been minimized.

Copy link
Member Author

commented Dec 23, 2018

OK, thanks for this summary, I will try to circle back with advocates of this variant and see their thoughts.

@bjunc

This comment has been minimized.

Copy link

commented Dec 23, 2018

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()
@mAAdhaTTah

This comment has been minimized.

Copy link
Contributor

commented Dec 23, 2018

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.

@littledan

This comment has been minimized.

Copy link
Member Author

commented Dec 23, 2018

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.

@littledan

This comment has been minimized.

Copy link
Member Author

commented Dec 24, 2018

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.

@isiahmeadows

This comment has been minimized.

Copy link
Contributor

commented Dec 24, 2018

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.)

@robgraeber

This comment has been minimized.

Copy link

commented Jan 17, 2019

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?

@zenparsing

This comment has been minimized.

Copy link
Member

commented Jan 17, 2019

A possiblity that combines Elixer-style first-arg passing with the syntactic structure of the bind proposal (as suggested above by @isiahmeadows).

Syntax:

CallExpression:
  CoverCallExpressionAndAsyncArrowHead `->` MemberExpression Arguments
  CallExpression `->` MemberExpression Arguments

Common usage scenario:

import { map, filter, collect } from 'iter-funs';
someIterator->map(cb1)->filter(cb2)->collect().forEach(cb3);

Pros:

  • Allows ergonomic chaining of -> and .
  • Functions work well with or without the syntax. Developers don't have to write a specific version of functions to match the pipeline style (e.g. underscore just works).

Cons:

  • Usage of -> might be surprising for those with C++ experience.
  • Does not offer native support for "point-free" programming styles.

Thoughts?

@charmander

This comment has been minimized.

Copy link

commented Jan 17, 2019

resolved

Developers don't have to write a specific version of functions to match the pipeline style (e.g. underscore just works).

They do, it just happens to be what’s used by e.g. underscore. For something existing that won’t match, see current RxJS.

@zenparsing

This comment has been minimized.

Copy link
Member

commented Jan 17, 2019

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.

@isiahmeadows

This comment has been minimized.

Copy link
Contributor

commented Jan 18, 2019

@zenparsing

  • Usage of -> might be surprising for those with C++ experience.

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 -> is effectively our ..

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:

  • Languages where partial application results in automatic currying:
    • Haskell uses function composition and occasionally x & f a b(f a b) x
    • OCaml uses x |> f a b(f a b) x and some use a userland function composition operator, usually defined as f @ glet (@) f g = fun x -> f (g x)
    • F# uses x |> f a b(f a b) x almost exclusively
  • Languages where partial application does not result in automatic currying:
    • Elixir uses x |> f(a, b)f(x, a, b)
    • Clojure uses (-> x (f a b))(f a b x), (->> x (f a b))(f x a b)

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.

@isiahmeadows

This comment has been minimized.

Copy link
Contributor

commented Jan 18, 2019

And another pro with a->b(...xs)b(a, ...xs): a->await b(...xs) is pretty obviously not ambiguous, and it obviously evaluates to await b(a, ...xs). It dodges the whole issue of how to handle async and yield by just not letting the RHS be just any expression.

@isiahmeadows

This comment has been minimized.

Copy link
Contributor

commented Jan 18, 2019

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.

@littledan

This comment has been minimized.

Copy link
Member Author

commented Jan 18, 2019

For more context, -> has been previously discussed for a short function literal that doesn't bind this, but I am not convinced we should add a feature for that (given how confusing this is already).

I'm not sure the property access confusion concern is C++-specific, as @zenparsing has specifically advocated a pattern like this for private state.

@gilbert

This comment has been minimized.

Copy link
Collaborator

commented Jan 18, 2019

@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 -> for their pipeline function. This is especially interesting considering the operator syntax was originally |>.

@mrsufgi

This comment has been minimized.

Copy link

commented Jan 28, 2019

didn't quite understand why -> works and |> don't.
״We also cannot use the |> operator here, since the object comes first in the binding. But -> works!״

@isiahmeadows

This comment has been minimized.

Copy link
Contributor

commented Jan 29, 2019

@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 x->f(g)f(x, g) for both JS and native targets, despite x |> f(g)f(g, x) already existing as a viable, working alternative.

@mAAdhaTTah

This comment has been minimized.

Copy link
Contributor

commented Jan 29, 2019

@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. ->), although I don't know how the committee would feel about 2 (or potentially 3, if we include bind) operators for various forms of pipelining.

@littledan

This comment has been minimized.

Copy link
Member Author

commented Jan 30, 2019

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.

@rbuckton

This comment has been minimized.

Copy link

commented Jan 31, 2019

I'm concerned introducing or using an Elixir-style operator would mean TC39 blessing first-arg semantics which would have repercussions for existing userland libraries like Ramda that use last-arg semantics. That was one of the motivators for partial application, in that you could place ? in either first-arg or last-arg positions.

@zenparsing

This comment has been minimized.

Copy link
Member

commented Feb 1, 2019

It's a good point @rbuckton. Favoring a certain style is unavoidable unless we go with something like hack-style/smart-mix or couple pipelining with partial application.

We do have to consider that coupling proposals multiplies risk, though.

@js-choi

This comment has been minimized.

Copy link
Contributor

commented Feb 1, 2019

For future readers: If @mAAdhaTTah’s #143 (comment) and @littledan’s #143 (comment) are confusing, it’s because I deleted a comment in which I mused about someone reviving the old “split-mix” system in a formal proposal, in which different pipe styles (Elixir, F#, Hack) are used by different operators, which would favor no particular style. I deleted it because I wanted to think about it more before anyone saw it but it looks like it was too late, heh. It seems to probably be a nonstarter anyway.

I do think that not favoring a particular system is an important goal. It’s a big part of why I’m enthusiastic about any proposal that can comfortably accommodate multiple/mixed styles (whether “smart” or “split” or whatever). I think there are still unexplored ways of reconciling different mixed styles, including mixed styles supporting Elixir style.

@chenglou

This comment has been minimized.

Copy link

commented May 13, 2019

I maintain Reason, mentioned by @gilbert. Someone asked me to justify it here, so here I am.

There are very nuanced but important distinctions to have pipe first rather than last, that warranted going the extra length of adding it rather than using the existing |>.

The more important issue is outlined e.g. here. Type inference, and general editor tooling, don't work well at all with last-arg, akin to the issue of import foo from bar rather than from bar import foo: you need some epic analysis to automplete foo in the former case. The truth is that all the tools are top-down left-right biased. For example, map(a => addInts(a, b), myStringArray) will give incorrect hint (from the compiler, editor tooling including autocompletion and type hint, refactoring, etc) that myStringArray has the wrong type. It's usually the case that the function is the wrong type (data over functions, functions over macros, etc). map(myStringArray, a => addInts(a, b)) will correctly warn at the more local addInt part.

FP literature kinda traditionally ignored these ergonomic problems, because "as long as the compiler errors it's theoretically fine". For a real-world, user-friendly production language, the quality of the compiler's help matter a lot. You start using different designs at that point.

To generalize this, if we check the FP languages themselves, especially those with currying, we'd realize that arg-last isn't well-defined (in the most serious sense). Consider:

int => float => string => int

It looks like string is the last argument (last int is the return type). But look:

type t = string => int 
type myFunc : int => float => t

Now it looks like float is the last argument! It's not just a matter of looks. These languages usually can't tell the difference. Consider if t was polymorphic and you couldn't just inline the types or something. So, in a curried language, you can only know which one is the first argument, but can not tell which one is the last argument. The wrecking of editor tooling earlier is one of the many manifestations of this.

The history of arg-last in JS libraries are to emulate actual FP languages' semantics. In turn, the semantics of arg-last in those languages are usually undefined in the context of arg-last pipe. Imo it'll be legacy decision in the making, to go with arg-last pipe for JS.

Lastly, the awkward and unspoken argument for arg-last comes from the fact that some find neat little elegance in the pipe in conjunction with automatic partial application. That's not a thing in JS (clarification: currying can be, but we're taking about auto partial application, when you call the function). And if we're even on the topic "neat parallels", OO methods being just a regular function that takes an object as first argument, seems like a more "elegant" analogy for FPs to make, rather than some other neat-parallel-with-currying-but-not-really-that-elegant analogies anyway.

Honestly, take it from the creator of TypeScript from the link I provided above. That person seems to know what he's doing =).

@tbremer

This comment has been minimized.

Copy link

commented May 15, 2019

@chenglou thanks for the thorough response. I have always wondered why Bucklescript supported both operators.

@isiahmeadows

This comment has been minimized.

Copy link
Contributor

commented Jul 5, 2019

I'd like to bump the original request of adding the Elixir-style semantics of x |> f()f(x) + x |> f(...)f(x, ...) to the list of considered alternatives, just to match reality. I've seen it talked about in the notes as well as extensively in the issues, but the README and wiki don't seem to match this reality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.