Skip to content

Instantly share code, notes, and snippets.

@tabatkins
Last active Sep 16, 2021
Embed
What would you like to do?
Comparing the three pipeline proposals

We've been deadlocked for a while on the pipeline operator proposal, for a few reasons. Partially it's just low implementor interest, as this is fundamentally just syntactic sugar, but also because there are three competing proposals and the proponents of each haven't been convinced by the others yet.

In this essay I hope to briefly outline the problem space, summarize the three proposals, and talk about what's gained/lost by each of them. (Spoiler: they're all nearly identical; we're arguing over very small potatoes.)

What is the Pipeline Operator?

When you call a JS function on a value, there are currently two fundamental ways to do so: passing the value as an argument (nesting the functions if there are multiple calls), or calling the function as a method on the value (chaining more method calls if there are multiple).

That is, three(two(one(val))) vs val.one().two().three().

The first style, nesting, is generally applicable - it works for any function and any value. However, it's difficult to read as the nesting increases: the flow of execution moves right-to-left, rather than the left-to-right reading of normal code execution; if there are multiple arguments at some levels it even bounces back and forth, as your eyes jump right to find a function name and left to find the additional arguments; and editting the code afterwards can be fraught as you have to find the correct place to insert new arguments among many difficult-to-distinguish parens.

The second style, chaining, is only usable if the value has the functions designated as methods for its class. This limits its applicability, but when it applies, it's generally more usable and easier to read and write: execution flows left to right; all the arguments for a given function are grouped with the function name; and editting the code later to insert or delete more function calls is trivial, since you just have to put your cursor in one spot and start typing or delete one contiguous run of characters with a clear separator.

The benefits of method chaining are so attractive that some very popular libraries contort their code structure specifically to allow more method chaining. (jQuery being the elephant in the room, as it's still the most popular JS library in the world, and its core design is a single über-object with a jillion methods on it, all of which return the same object type so you can continue chaining.)

The Pipeline operator attempts to marry the convenience and ease of method chaining with the wide applicability of function nesting. The general structure of all the pipeline operators is val |> one() |> two() |> three(), where one, two, and three are all ordinary functions that take the value as an argument; the |> glyph then does some degree of magic to "pipe" the value from the LHS into the function.

The three pipeline proposals just differ slightly on what the "magic" is, and thus on precisely how you spell your code when using |>.

Proposal One: F#-style

In this proposal, matching the F# language's pipeline syntax, the RHS of the pipeline is an expression that must resolve to a function, which is then called with the LHS as its sole argument. That is, you write val |> one |> two |> three to pipe val thru the three functions; the general syntax transform is left |> right => right(left).

Pro: The restriction that the RHS must resolve to a function lets you write very terse pipelines when the operation you want to perform is already a named function.

Pro: Only one new bit of syntax needs to be minted, the |> itself. (The others require a placeholder syntax as well.)

Con: The restriction means that any operations that are performed by other syntax must be done by wrapping the operation in an arrow func: val |> x=>x[0], val |> x=>x.foo(), val |> x=>x+1, val |> x=>new Foo(x) etc. Even calling a named function requires wrapping, if you need to pass more than one argument: val |> x=>one(1, x).

Con: The yield and await keywords are scoped to their containing function, and thus can't be handled by the arrow-func workaround from the previous paragraph. If you want to integrate them into pipeline at all (rather than requiring the pipeline to be paren-wrapped and prefixed with await), they need to be handled as a special syntax case: val |> await |> one to simulate one(await val), etc.

Proposal Two: Hack-style

In this proposal, matching the Hack language's pipeline syntax, the RHS of the pipeline is an expression containing a special placeholder variable, which is evaluated with the placeholder bound to the LHS value. That is, you write val |> one(#) |> two(#) |> three(#) to pipe val thru the three functions; the general syntax transform is left |> right(#) => right(left).

Pro: The RHS can be any expression, and the placeholder can go anywhere any normal variable identifier could go, so you can pipe to any code you want without any special rules at all: val |> one(#) for functions, val |> one(1, #) for multi-arg functions, val |> #.foo() for method calls (or val |> obj.foo(#), for the other side), val |> # + 1 for math, val |> new Foo(#) for constructing, val |> await # for awaiting promises, etc.

Con: If all you're doing is piping thru already-defined unary functions, it's slightly more verbose than F#-style since you need to actually write the function call syntax, adding a (#) to it.

Proposal 3: "Smart Mix"

In this proposal, mixing the previous two proposals, the RHS of the pipeline is either an expression containing a special placeholder variable, which is evaluated with the placeholder bound to the LHS value, or a bare (dotted-)ident which must resolve to a function, which is called with the LHS as its sole argument. That is, you write val |> one |> # + 2 |> three.foo to pipe val thru the expressions; the general syntax transform is either left |> right(#) => right(left) or left |> right => right(left).

Pros: It can handle all Hack-style cases with identical syntax, and additionally can handle most F#-style cases with identical syntax.

Con: If you're doing any higher-level programming to produce a function which'll eventually get called, you do need to use the placeholder; you can't rely on F#-style evaluation. That is, if you have a logger(fn) function that modifies a function to log its args and return value, you must pipe it like val |> logger(one)(#); writing val |> logger(one) is a syntax error. (This avoids garden-path issues, where it's not clear which syntax you're dealing with until you either spot the # or establish that there isn't one. Instead, the F#-style cases are trivial to spot because they're so restricted in syntax, and all the rest are Hack-style.)

Con: As a result of trying to split the difference between the two syntaxes, it ends up with a more complex mental model. If a pipeline RHS is currently written in F#-style, and you realize you want to do something else to it, you have to recognize that it must be shifted to Hack-style syntax. (Luckily it's a syntax error if you forget, rather than just evaluating to the wrong thing, so at least it's easy to recognize after-the-fact that this is needed.)

What's The Difference?

Ultimately... very little.

As long as F#-style has the special syntax forms for await and yield, then all the proposals can handle all the same cases with nearly identical syntax. They just pay a very small syntax tax for some cases vs the other proposals.

Specifically, F#-style is optimized solely for calling unary functions. Anything else pays a syntax tax of three characters over what Hack-style would do, with an x=> prefix to introduce the arrow-function wrapper. (This assumes the parsing issues are resolved to allow arrow-funcs as RHS without needing parens; this isn't certain to happen, as it involves some significant tradeoffs. If it doesn't happen, the tax is actually five characters, placed both before and after your code.)

Hack-style is optimized for arbitrary expressions, but if you're calling a named unary function, it pays a syntax tax of three characters over what F#-style would do, with a (#) suffix to actually invoke the function.

"Smart Mix" only pays a syntax tax (a (#) suffix, just like Hack-style) if it wants to invoke an anonymous unary function produced as the result of an expression. However, it pays a mental tax of having two separate syntax models.

...and that's it. That's all the difference is between the proposals. A three-character tax on RHSes in various situations.

My Preferences

Over time, I've become strongly in favor of Hack-style pipelines. I think that the case of "unary function" will in general be less common than "everything besides unary functions", so it makes more sense to put the tax on the first category rather than the second. (In particular, method-calling and non-unary function-calling are big cases that will never not be popular; I think those two on their own will equal or exceed the usage of unary functions, let alone including all the other syntax that Hack-style can do without a tax.)

I also think that why the tax is invoked makes Hack-style more usable; the syntax tax of Hack-style (the (#) to invoke the RHS) isn't a special case, it's just writing ordinary code in the way you normally would without a pipeline. On the other hand, F#-style requires you to distinguish between "code that resolves to an unary function" and "anything else", and remember to add the arrow-function wrapper around the latter case. val |> foo + 1 is still a syntactically valid RHS, it'll just fail at runtime because the RHS isn't callable. You can avoid having to make this recognition by always wrapping it in an arrow func, but then you're paying the tax 100% of the time and effectively just writing a slightly more verbose Hack-style.

This is also why I'm against "Smart Mix" - if you use the bare-ident F#-style case, and you realize you actually need to do anything more complicated, you need to remember to do a slight code rewrite beyond the actual change you want to make. (Tho it's a syntax error, not a runtime error, when you forget, so it's easier to catch.) You can avoid it by always writing Hack-style, but then there's no point to the mixture. (Tho at least if you do so, you're not paying the tax that F#-style does in all the other cases.)

All that said, the benefits of pipeline are so nice (again, part of the reason for jQuery's success was the syntax niceness of method chaining!) and the costs of each syntax's non-optimal case so low, that I'd happily take F#-style over nothing. So my preferences are Hack-style > Smart Mix > F#-style >>>> not doing pipeline at all.

@Pokute

This comment has been minimized.

Copy link

@Pokute Pokute commented Apr 4, 2021

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

@Pokute

This comment has been minimized.

Copy link

@Pokute Pokute commented Apr 4, 2021

I was kinda hoping for |#> to remain reserved for calling unary functions on pipeline, but it seems to be a bit too complex, since it would be valid, although rare, code when # is handled as an identifier. Can't see the same problem with #|> or |># though.

@Pokute

This comment has been minimized.

Copy link

@Pokute Pokute commented Apr 4, 2021

Still, my favourite will always be the one that eventually goes into EcmaScript, F#, Hack or Smart-style. When I finally get to use it freely after so many years, I'll immediately forget about the other possibilities.

@dustinlacewell

This comment has been minimized.

Copy link

@dustinlacewell dustinlacewell commented Apr 7, 2021

I came into this years ago staunchly supporting F# style (it's one of my favorite languages) but I slowly reconsidered this whole while. I think the above has firmly crystallized the hack style as making the most sense to me. You'll basically be creating lambda's all over the place (given the JS ecosystem we have) and so you're specifying the placeholder all the time anyway. Though with F#-style mixed with partial application one starts feeling strangely wistful for a better functional future that will never happen.....

Hack-style > Smart Mix > F#-style >>>> not doing pipeline at all

In 5 years, it's going to be a matter grand of existential bewilderment that TS still has no pipeline operator at all.

@peey

This comment has been minimized.

Copy link

@peey peey commented Apr 9, 2021

An argument in favour of F#-style over Hack-Style:

# will be a specially-introduced syntax which essentially acts as a placeholder. Parameters already act as a placeholder (and can take names other than #) x => f(x) or if you prefer to give more clarity to the code, then aBetterName => f(aBetterName).

Pros of re-uses language machinery (single parameter functions) to denote instead of introducing new one:

  1. Less burden on language tooling: on the interpreter, on syntax analysis tools, on static analysis tools. Since everything in the pipeline is bound to be a valid function instead of something new.
  2. Interop with existing language features: (e.g. param naming, arg destructuring) ... |> ({x, y}) => new Foo(x, y) |> .... To use arg destructuring with Hack-style you'd have to write ... |> () => { const {x, y} = #; return new Foo(x, y) } |> ...

A note on performance: the optimization of a single parameter function can be left to the interpreter, it'll have as much information for x => some expression containing x as for some expression containing #.

My Opinion

From a language design perspective, these seems like big enough potatoes to argue on.

"Smart" mix feels over-engineered and an unnecessary compromise for such a simple feature. My preferences would be F#-style > Hack-style >>>> not doing pipeline at all > Smart Mix


Edit: have added an issue on the proposal pipeline for a concrete case/example (arg destructuring) that embodies my argument above js-choi/proposal-hack-pipes#4. Thanks to the author of this gist for starting a conversation comparing the proposals.

@anatoliyarkhipov

This comment has been minimized.

Copy link

@anatoliyarkhipov anatoliyarkhipov commented Apr 26, 2021

...and that's it. That's all the difference is between the proposals.

...eeeeeeeexcept there is a bajillion libraries like Ramda already used in production by FP-programmers, and having to have their own pipe() functions due to the lack of the syntax.

@nmn

This comment has been minimized.

Copy link

@nmn nmn commented May 26, 2021

Somewhat ironically, reading your thoughts on why you prefer Hack-Style pushed me in favor of F# style.

You can ... by always wrapping it in an arrow func, but then ... effectively just writing a slightly more verbose Hack-style.

This makes me realize that the only downside of F# style is you have to write 3 extra characters per pipe.
|> f(#) vs |> _ => f(_)

This combined with the cluster**** that is the discussion about what should be used at the placeholder further pushes me in favor of F# style. (FWIW # seems like the best option so far. The current leader ? seems very confusing within Javascript where it already has a bunch of meanings.)

F#-style has less cognitive overhead.

  1. It's just a new operator.
  2. The RHS is just a function
  3. You can use arrow functions as always, no new syntax, no new strange character to understand.
  4. Better readability as things look more similar to method chaining
  5. Easily debuggable - We know how to add breakpoints within functions.
  6. Runtime semantics make more sense.

The downside of F#-style

So the big thing that bugs me about the F# style is that it doesn't fix well with method calls.

value 
  |> myFunction
  .myMethod()

This syntax no longer work correctly as it would be interpretted as myFunction.myMethod()(value) which is almost definitely wrong.

Hack style does this better:

value 
  |> myFunction(?)
  .myMethod()

is interpretted as myFunction(value).myMethod which is definitely what we would want.

Of course this can easily be solved by wrapped method calls in arrow functions as well, so it's not a big deal to me.

value 
  |> myFunction
  |> x => x
     .myMethod()
     .myOtherMethod()
  |> otherFunction

Still would take either proposal

I 100% agree on this part. It's been so many years that I don't care which proposal it is, I just want those sweet, sweet pipeline operators.

@tabatkins

This comment has been minimized.

Copy link
Owner Author

@tabatkins tabatkins commented May 26, 2021

This makes me realize that the only downside of F# style is you have to write 3 extra characters per pipe.

That's not the only con; you have to handle await/yield specially in F#-style as well (and you can't interleave them in an expression; you must break the expression apart into an "inside await" and "outside await" part, so they can sit in separate pipeline bodies with a |> await |> between them).

But yeah, in most situations the only con is the three-character tax. (Assuming that the parsing issues allowing unwrapped arrows are worked out. Otherwise it's a 5-char tax, split over both sides of the expression - val |> # + 1 vs val |> (x=>x+1).)

So the big thing that bugs me about the F# style is that it doesn't fix well with method calls.

Oof, I hadn't run into that before, that's annoying. Smart-mix suffers in the same way, if you're grabbing a property at the end instead of calling a method.

@nmn

This comment has been minimized.

Copy link

@nmn nmn commented May 27, 2021

That's not the only con; you have to handle await/yield specially in F#-style as well

I actually consider that a feature of the F#-style. It forces you to think clearly about what you’re awaiting/yielding. But I can see how you might consider it syntactic noise.

Assuming that the parsing issues allowing unwrapped arrows are worked out

I would be really bummed if this isn’t the case. Being forced to wrap every arrow function in parentheses might push me over to Hack-style again.

but yeah, happy with both implementations if it just moved forward.

@lightmare

This comment has been minimized.

Copy link

@lightmare lightmare commented Jul 2, 2021

Pros of re-uses language machinery (single parameter functions) to denote instead of introducing new one:

  1. Less burden on language tooling: on the interpreter, on syntax analysis tools, on static analysis tools. Since everything in the pipeline is bound to be a valid function instead of something new.

Except that the F# proposal is headed towards changing the grammar more heavily than Hack's new topic token. And when it comes to static analysis, syntax is irrelevant.

  1. Interop with existing language features: (e.g. param naming, arg destructuring) ... |> ({x, y}) => new Foo(x, y) |> .... To use arg destructuring with Hack-style you'd have to write ... |> () => { const {x, y} = #; return new Foo(x, y) } |> ...

There is no issue with destructuring. Your Hack example is deliberately verbose and not even correct. It would be:

... |> (({x, y}) => new Foo(x, y))(#) |> ...
@tabatkins

This comment has been minimized.

Copy link
Owner Author

@tabatkins tabatkins commented Jul 2, 2021

I don't know if that last Hack example is very readable; I think a do-expr with destructuring is indeed better, if that's what's needed.

Or, of course, you could write it out manually, if the names weren't too long:

... |> new Foo(#.x, #.y) |> ...

In many cases that'll be the most readable way to write this.

@lightmare

This comment has been minimized.

Copy link

@lightmare lightmare commented Jul 2, 2021

Absolutely, that's how I'd write it as well. I just wanted to correct the record, that "to use destructuring with Hack style" you don't need a variable declaration and a return statement.

@pvarga-dni

This comment has been minimized.

Copy link

@pvarga-dni pvarga-dni commented Jul 2, 2021

How about null/undefined-handling? As an analogy to maybeNull.prop vs maybeNull?.prop, consider:

maybeNull ?|> func which would be equivalent to something like (maybeNull != null) ? func(maybeNull) : maybeNull.

This is somewhat similar to the >>= operator in Haskell (or maybe even more similar to <&>).


Another idea that just popped into my head is generalizing # so that it becomes a "wildcard" (or "hole") even outside of a "piping" context.

(1 + # / 3) would be the equivalent of (n => 1 + n / 3).

And then this "wildcard" might also benefit from graceful null handling, so that perhaps (1 + ?# / 3) would be sort of equivalent to
(n => (n != null) ? 1 + n / 3 : n).

Then a natural extension is to allow for multiple "parameters", not only # but #1, #2 etc.

@tabatkins

This comment has been minimized.

Copy link
Owner Author

@tabatkins tabatkins commented Jul 2, 2021

Any additional operator variants need to really justify their weight, imo; this is a space where it's extremely easy to fall into unreadable grawlix and we're already skirting the line, saved mostly by the visual simplicity of the |> triangle. I don't think any proposed variant has been worthwhile so far; they just don't correspond to actions that I've seen in my or other's code often enough to justify the additional complexity.


Implicit arrow functions are also a non-starter, since it's completely unclear from a quick, or often even a detailed, reading what the bounds of the arrow end up being. And again, we have to compare it against existing syntax to see if it's actually saving us much of anything, and here I think it's clear that it's not - you just get to omit the three-character prefix of the arrow function. Saving three characters, at the cost of significantly reduced clarity (and a very unclear/confusing interaction with normal pipelines), isn't worthwhile.

@nmn

This comment has been minimized.

Copy link

@nmn nmn commented Jul 3, 2021

This again re-inforces my preference for F# style. It makes things like forwarding Nulls etc easy with higher-order functions. Since Hack style uses expressions and not functions, it’s not easy to wrap each step.

  1. Forwarding Nulls:
const forwardNull = (fn) => x => x == null ? x : fn(x)

value
  |> forwardNull(fn1)
  |> forwardNull(fn2)
  

This removes the need for adding ever more operators or syntax. It makes what’s happening clear unlike Haskell with its cryptic operators. And it’s easy to create any kind of higher-order-function for any situation. Forwarding nulls or errors. Error handling, whatever.

Saving three characters, at the cost of significantly reduced clarity (and a very unclear/confusing interaction with normal pipelines), isn't worthwhile.

sounds like an argument for F# style to me!

@tabatkins

This comment has been minimized.

Copy link
Owner Author

@tabatkins tabatkins commented Jul 3, 2021

Functions are expressions too, you know.

const forwardNull = (fn, x) => x == null ? x : fn(x)

value
  |> forwardNull(fn1, #)
  |> forwardNull(fn2, #)
  

Or if you really wanted to use the curried form of the function for some reason, forwardNull(fn1)(#) works just fine too.

And if you're instead just doing this as a one-off:

value
  |> # == null ? # : fn(#)
  ...

// vs
value
  |> x=> x == null ? x : fn(x)
  ...

I'm not sure if you avoided reading the essay in this gist or what, but as I said up above (and gave multiples examples for), F# and Hack are identical in power and expressiveness, with the only difference being whether you have to add an additional three characters in some situations vs others. (Ignoring some edge cases around await where F# is forced to pay additional tax.) There is no need to add "ever more operators or syntax", or at least, if something is worth adding, it's equally worthy in either syntax.

If you're going to try and score internet points, please do me the basic respect of actually reading the topic first. It's a waste of both of our time otherwise.

@nmn

This comment has been minimized.

Copy link

@nmn nmn commented Jul 3, 2021

I definitely read the essay above. My comment was specifically in reply to @pvarga-dni. I was actually agreeing with you.
I wanted to just show that with HOCs and the Pipeline operator, we don’t need Haskell’s >>= or <*> operators.

Since Hack style uses expressions and not functions, it’s not easy to wrap each step.

I did totally have a brain fart when I said this. So apologies on not thinking it through. HOCs definitely work with Hack-style pipeline too. Sorry about the churn.

@tabatkins

This comment has been minimized.

Copy link
Owner Author

@tabatkins tabatkins commented Jul 3, 2021

Apology accepted! I just get weary of people thinking there's something special and simpler about F#-style and then being kinda smug about it, when it's simply untrue.

@pikeas

This comment has been minimized.

Copy link

@pikeas pikeas commented Jul 5, 2021

[any ordering of proposals] >>>> not doing pipeline at all

This is the meat of it. Any implementation of a pipeline operator will be a boon to JS/TS development. I'd love to see this added to the language, hopefully before 2031.

@theScottyJam

This comment has been minimized.

Copy link

@theScottyJam theScottyJam commented Jul 18, 2021

Often, we tend to talk about the cost of a particular feature by "how many characters it takes". While this is certainly important, I feel like often a more important metric (which is harder to measure) is "how visually taxing is it to read this thing"?

Lets compare these three examples:

// Today
rolesFromGroups(groupsFromUser(getUser(id), true))

// F#
id |> getUser |> u => groupsFromUser(u, true) |> rolesFromGroups

// Hack-style
id |> getUser(?) |> groupsFromUser(?, true) |> rolesFromGroups(?)

Yes, hack-style has to pay a 3 character tax with "(?)", and F# style has to pay a 5 character tax (including whitespace) with "x => ", but what's important here is that both versions end up a whole lot longer than the function-call version, and yet we find the pipe operator more readable.

Here's another way to look at it. Compare these two ridiculous pipeline operator ideas. One syntax is to use |next> and the other, shorter one is to use % @. You'll find that the longer |next> one is more readable because it's a single unit, while "% @" adds a whole lot of visual clutter.

id |next> getUser(?) |next> groupsFromUser(?, true) |next> rolesFromGroups(?)

id % @ getUser(?) % @ groupsFromUser(?, true) % @ rolesFromGroups(?)

In a very similar vein, I argue that, even though x => isn't that many extra characters, it turns a single pipe operation into three separate units - a "|>", "x", and "=>", which adds a lot of visual clutter.

id |> x => getUser(x, false) |> u => groupsFromUser(u, true) |> g => rolesFromGroups(g, false)

Gross 🤮️

However, I think F# can be saved. Every once in a while, it becomes common for developers to make pseudo-operators, by combining different operators together into one. In Javascript we have the "to boolean" operator !!, C++ has the "down-to" operator --> which is -- + >, etc. Some people shun pseudo-operators like these, but when everyone understands what they do, it can make code easier to read. If the community happened to adopt the "fancy arrow" pseudo-operator (|>$=>), then that would greatly reduce the visual tax of F#-style while providing hack-style support to F#-style.

id |>$=> getUser($, false) |>$=> groupsFromUser($, true) |>$=> rolesFromGroups($, false)

We could encourage this kind of thing right from the start (for any project that wishes to adopt it) and create a nice MDN page for the "fancy arrow operator" for the googlers looking for it, add new linter rules that special-case that kind of spacing, etc. The up-side is that we're not actually adding a new topic-style concept that only ever gets used by a single operator, instead, we're just building on existing principles.

It's a stretch of an idea, but it works 😅️.

@nmn

This comment has been minimized.

Copy link

@nmn nmn commented Jul 18, 2021

I'm more and more convinced that any differences between the two proposals are now minor. The question for me is what we can do to move the process forward?

@theScottyJam

This comment has been minimized.

Copy link

@theScottyJam theScottyJam commented Jul 19, 2021

I ended up mentioning this "fancy arrow" idea over in an issue here, to see if others think it helps improve the usability of F#-style too.

Ultimately, I agree @nmn - it would be nice to just get something in - I think the differences between the two are pretty minor.

@dustinlacewell

This comment has been minimized.

Copy link

@dustinlacewell dustinlacewell commented Jul 19, 2021

Let's flip a coin.

@Avaq

This comment has been minimized.

Copy link

@Avaq Avaq commented Jul 20, 2021

Another "pro" I'd like to see mentioned for the F# proposal is that it's very simple to extract code from a pipeline. A pipeline is essentially just (expression1) |> (expression2) |> (expression3), where any of these expressions can be extracted as-is and replaced by the name they were given:

const myExpression = (expression2)

(expression1) |> myExpression |> (expression3)

With the Hack or Smart proposals, extracting code from (or the reverse: in-lining code into) a pipeline is not as straight-forward, because once you move your expression to a parent scope, the meaning of the topic identifier will change: either to the outer topic, or to be a syntax error altogether.


And a "con" I haven't seen mentioned for the Smart proposal in particular is that its "tacit style" foregoes referential transparency, further decreasing ease of refactoring / making modifications to code that uses it.

@OliverJAsh

This comment has been minimized.

Copy link

@OliverJAsh OliverJAsh commented Aug 4, 2021

👍 to what @Avaq said about extracting code, in favour of the F# proposal. For example:

val
  |> one
  |> two
  |> x=>three(1, x)

If we wanted to extract x=>three(1, x) to a named function called threeWrapper, this would be very easy because x=>three(1, x) is valid syntax both inside and outside of the pipe operator. We can copy and paste it into a variable declaration and be done with it. Here's a demonstration using the pipe function:

Screen.Recording.2021-08-05.at.00.31.54.mov

Under the Hack proposal, this sort of refactoring/extracting would be much more awkward, because three(1, (#)) is not valid syntax outside of the pipe operator. We can't simply copy and paste it into a variable declaration like we could with x=>three(1, x).

val
  |> one(#)
  |> two(#)
  |> three(1, (#))

At Unsplash we have thousands of usages of the pipe function and we perform refactors/extractions like this on a daily basis, so I think it is quite an important thing to consider.

@tabatkins

This comment has been minimized.

Copy link
Owner Author

@tabatkins tabatkins commented Aug 5, 2021

While it's not technically a trivial copy-paste, it's very nearly so. Given:

val
  |> one(#)
  |> two(#)
  |> three(1, #)

you can extract it by prepending an arrow-func with an appropriately-chosen dummy argument that's just immediately piped:

const threeWrapper = x=>x|>three(1, #);
val
  |> one(#)
  |> two(#)
  |> threeWrapper(#)

(Or, of course, you can rename # to the arg and avoid the pipe, but that's a step further.)


Note as well that, in Hack style, you can extract multiple pipeline steps at once in exactly the same way as you extract one:

const twoThreeWrapper = x=>x|>two(#)|>three(1, #);
val
  |> one(#)
  |> twoThreeWrapper(#)

Whereas in F#-style you have to do slightly more rewriting to extract multiple steps, performing essentially the same transformation as Hack-style:

val
  |> one
  |> twoThreeWrapper

So this is yet another situation where the two are essentially identical, and when tooling is involved there's no reason to consider them different at all.

@OliverJAsh

This comment has been minimized.

Copy link

@OliverJAsh OliverJAsh commented Aug 5, 2021

(Or, of course, you can rename # to the arg and avoid the pipe, but that's a step further.)

In my experience using the pipe function, when we're talking about extracting a single step of the pipeline, we almost always want to extract a function inside the pipeline (i.e. x=>three(1, x)), rather than a pipe expression with a single step (i.e. x=>x|>three(1, #)), so this is the use case I'm focusing on. This is where I think the Hack style suffers for the reasons I explained above. Keep in mind this tax is paid not just when extracting but also when inlining.

@jleider

This comment has been minimized.

Copy link

@jleider jleider commented Aug 5, 2021

While there have been good arguments on both sides: Hack vs F#; I would like to add my 2¢. I am in favor of the F# style pipe operator due to its simplicity and it keeping in line with other languages that have first class function support.

Also, after reading through the comments, most of them seem to either be unopinionated or be in favor of F# rather than Hack. The exception being the OP @tabatkins who clearly favors Hack style.

F# >>>> no pipe operator > Smart Mix > Hack
@theScottyJam

This comment has been minimized.

Copy link

@theScottyJam theScottyJam commented Aug 5, 2021

I do agree with @tabatkins that hack-style is a better fit for the Javascript ecosystem than F#-style. F#-style works better in a curried language. Javascript is not a curried language. This means that almost every time you use an F#-style pipe, you're also going to be creating a function literal, which is just extra noise that doesn't need to be there.

I personally like hack-style more. however, I struggle bringing myself to accept the syntax cost of it (and the overall extra complexity of it). We would be adding another meaning to one of the precious few ascii symbols available to us in Javascript, and I'm not sure that's worth it for a single operator. For this reason alone I lean slightly towards the F# crowed (but I'm pretty torn, and keep changing my mind).

@tabatkins

This comment has been minimized.

Copy link
Owner Author

@tabatkins tabatkins commented Aug 5, 2021

Also, after reading through the comments, most of them seem to either be unopinionated or be in favor of F# rather than Hack.

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

@theScottyJam

This comment has been minimized.

Copy link

@theScottyJam theScottyJam commented Aug 11, 2021

Here's another point in favor of F#-style that I just thought of. It can be nice to provide names to the variables of intermediate steps in contexts where it's easy to lose sight of what that variable represents. (I know point-free programming is nice, but there's value in explicit naming too).

const adultNamesToAge = getUsers()
  |> Object.values(%)
  |> %.filter(x => x.age >= 18)
  |> %.map(x => [x.name, x.age])
  |> Object.fromEntries(%)

const adultNamesToAge = getUsers()
  |> idToUser => Object.values(idToUser)
  |> users => users.filter(x => x.age >= 18)
  |> adults => adults.map(x => [x.name, x.age])
  |> entries => Object.fromEntries(entries)

I know the first example is more concise, but it's much easier to look in the middle of the second example's pipeline and see what's going on, as more context is given to you. OK, OK, I realize in the first example I could have named the "x" variable something better to give more context, but that's not always possible.

I realize that additional context like this isn't always needed or wanted, but there are times when it can be useful.

@sdegutis

This comment has been minimized.

Copy link

@sdegutis sdegutis commented Aug 13, 2021

(Throwing in my 2¢) Just skimmed this gist and I'm pretty sold on the Hack style proposal.

@euan-smith

This comment has been minimized.

Copy link

@euan-smith euan-smith commented Sep 1, 2021

The deciding factor, for me, is the handling of await and yield. If it wasn't for that I would be torn for a whole number of reasons, but that sways it each time.

For example, the argument about extracting code from @Avaq is persuasive, until you consider how to deal with url |> fetch |> await |> etc. In fact, as a common example where I would use this a lot, I think it is worth showing how this case would work for both.
url |> fetch |> await |> r => r.json() |> await |> handleJson (F#)
vs
url |> await fetch(%) |> await %.json() |> handleJson(%) (Hack)

I find the hack style much closer to how I would express things in JS normally.

Hack > F# >> Smart Mix >>>> no pipe operator

@sdegutis

This comment has been minimized.

Copy link

@sdegutis sdegutis commented Sep 1, 2021

It looks like someone's already worked on them in https://babeljs.io/docs/en/babel-plugin-proposal-pipeline-operator and we can try them now in real life code using Babel, in which case we can get a better feel for it over time and then the community could have more empirical evidence for deciding on one, although I'm already convinced Hack is the best style and should be adopted. But at least this means (if my understanding of that Babel plugin is correct) that we can use the one we prefer already today with Babel.

@OliverJAsh

This comment has been minimized.

Copy link

@OliverJAsh OliverJAsh commented Sep 1, 2021

@euan-smith

url |> fetch |> await |> r => r.json() |> await |> handleJson (F#)
vs
url |> await fetch(%) |> await %.json() |> handleJson(%) (Hack)

Or we can just use Promise.prototype.then instead of async/await, which is even more concise and still reads left-to-right:

fetch(url).then(r => r.json()).then(handleJson)

If it's more concise to use Promise.prototype.then, maybe we can live without async/await support inside the pipeline operator, rather than trying to shoehorn it in?

If you absolutely want to use the pipeline operator with async functions, you can still do it by writing a wrapper function around Promise.prototype.then, such as Task.chain in fp-ts. Pseudo code:

url |> fetch |> T.chain(r => r.json()) |> T.chain(handleJson)
@euan-smith

This comment has been minimized.

Copy link

@euan-smith euan-smith commented Sep 1, 2021

@OliverJAsh on that basis you can just use the promise then method for any pipelining:
a |> a=>a+1 |> Math.sqrt could be done now with Promise.resolve(a).then(a=>a+1).then(Math.sqrt) even though none of it is async.

Adding wrapper functions from a library just reduces the ability of somebody coming along and just reading the code. No more, please. as @theScottyJam quite correctly pointed out above, the issue here is not about concise code, it is about readability and maintenance.

The whole point of this is to make code more readable, more maintainable, less prone to errors. async operation is absolutely at the heart of much of what JS does and we've seen a massive improvement going from callback-hell to thenables to native promises to generator-based coroutines to async/await. Now async functions look nearly the same as non-async and the same methods can be used with both in the same way.

I very firmly think that any new JS syntax feature MUST be async first. Right now we can do (almost?) anything with async we can do otherwise, and it has taken work to get there. Adding something which doesn't work with async is a step backwards, let's not do that. The original F# proposal without await and yield is, for me, an absolute non-starter. With async support it is OK, I was just pointing out that one of the downsides of F# with async is that it requires new syntax and that then seems to counteract the code-extraction argument. For me, that makes Hack-style a better proposal.

@sdegutis

This comment has been minimized.

Copy link

@sdegutis sdegutis commented Sep 1, 2021

Why don't other people's gist comments let us do thumbs-up on them? I fully agree that pipeline must be async first and not just use a hack or workaround that will then be included as an npm lib in practically every pipeline project. It'll also make TypeScript typing of it better since it'll be built into the language so tsc will be able to do special things in the compiler that can't be done in a typings lib.

@benlesh

This comment has been minimized.

Copy link

@benlesh benlesh commented Sep 7, 2021

Honestly, the one of the biggest problems with the Hack-style pipeline that I see is you can't possibly create a suite of "pipeable" functions now that will work well with Hack pipeline later. Basically, Hack pipeline is DOA and any library that was trying to leverage functional piping will need to rewrite themselves in order to accommodate it. Any library out there that was helping push functional programming in JavaScript will be left swinging in the wind by JavaScript's first functional operator.

For example, RxJS will need to refactor all of its operators to accommodate the Hack pipeline, then everyone else will have to rewrite their RxJS code to adjust to this new paradigm. Ironically, google3 will be extremely hard hit when this comes to pass. It will probably cost Google a fortune to migrate, if they even can. More than likely, they'd just end up stuck on an older version of RxJS for a long time, or using some sort of local mods.

With the F# pipeline, any function that is currently pipeable with a standard functional pipe will "just work". Meaning zero migration for existing libraries to leverage F# pipeline OOTB.

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

@sdegutis

This comment has been minimized.

Copy link

@sdegutis sdegutis commented Sep 7, 2021

Personally I think that's a fine trade-off. Libraries are come and go. They have a lifespan of maybe 5-10 years max. Nobody is still using MooTools and jQuery is even on its way out. Even React will eventually be replaced with something else one day, probably.

But JavaScript is currently and probably indefinitely the long term de facto language of the web, and should not be held back because of any limitations that existing but overall shorter-lived libraries would have because of language evolution.

Besides, code is much easier and quicker to write than you made it sound. With the right college student having the right inspiration, it could be a simple weekend project to create a RxJS clone that's compatible with the new (Hack) pipelines.

@euan-smith

This comment has been minimized.

Copy link

@euan-smith euan-smith commented Sep 7, 2021

@benlesh absolutely good points. How would you fix async in this case? The proposal of having |> await |> doesn't seem to me to fit into what you describe either.

@theScottyJam

This comment has been minimized.

Copy link

@theScottyJam theScottyJam commented Sep 7, 2021

@benlish

Some of your points you brought up against hack-style pipes are exactly the same arguments I have against F#-style pipes.

Honestly, the one of the biggest problems with the Hack-style pipeline that I see is you can't possibly create a suite of "pipeable" functions now that will work well with Hack pipeline later

One of the biggest advantages of the hack-style pipeline operator is that there is no difference between a "pipeable" function and a "non-pipeable" function. There's no such thing as making a suite of pipeable functions. The hack-style pipe works with how Javascript is used today - you can take any existing function and throw it into the pipeline operator, and it works in a very natural way.

Basically, Hack pipeline is DOA and any library that was trying to leverage functional piping will need to rewrite themselves in order to accommodate it.

And if we go with F#-style pipelines, does everyone else, standard library, third-party libraries, etc, need to rewrite themselves so that they provide curried versions of their different functions, that would fit well in the pipeline operator?

Any library out there that was helping push functional programming in JavaScript will be left swinging in the wind by JavaScript's first functional operator.

Not really. Using curried functions in a pipeline operator really isn't that bad.

const result = data
  |> transform(^)
  |> anotherTransform(^)

Is that extra (^) at the end of each step really enough to kill the usability of the pipeline operator? Perhaps it's not quite point-free programming, but it's really not that far off - you don't lose any of the advantages to point-free programming doing this. Sure, it's different and a little more verbose from other languages, but I really don't think that's killing the readability of the operator.

For example, RxJS will need to refactor all of its operators to accommodate the Hack pipeline, then everyone else will have to rewrite their RxJS code to adjust to this new paradigm.

This sounds extreme. Javascript is not a curried language, never was, and never well be. If people want to use it as one, that's fine. But, these curried libraries already know from the start that they're going against the grain. People who are using them also understand that they're going against the grain. There's always going to be a cost for doing that - e.g. whenever you want to use a non-curried function (which Javascript is full of), you'll need to transform it to a curried one first. The (^) is also just part of the cost of using Javascript in a less-standard fashion.

I don't want that last paragraph to sound too harsh - of course, it's important for the committee to be mindful of those who like to use Javascript in a curried fashion - they're part of the userbase too. But this operator is for everyone, not just the extreme functional users, and it's also important for the comittee to be mindful of everyone else who wants to use the pipeline operator without currying everything. In other words, there's many advantages that the F#-style pipe operator has over hack-style, but the argument "This is curry-friendly, at the cost of being difficult to use with non-curried functions" just does not hold a lot of water in the Javascript ecosystem.

@tabatkins

This comment has been minimized.

Copy link
Owner Author

@tabatkins tabatkins commented Sep 7, 2021

you can't possibly create a suite of "pipeable" functions now that will work well with Hack pipeline later.

You definitely can.

If you really want to focus on maximal ease-of-use in both pipe() and |>, most libraries focused on HOFP that I've seen have an "auto-currying" function that transforms a traditionally-defined function into one that can either take all its argument at once or in multiple invocations; running this over a library will do just fine. It's not hard to write yourself, depending on how fancy you want to get; if you're just targetting "auto-curry the final argument only", it's quite straightforward. (Doing it by hand for minimal extra runtime cost is also quite easy, tho it is a little bit of boilerplate.)

But you can also just privilege one or the other, if desired, with minimal weight added on the other side:

  • you can take functions already written for pipe() and call them as pipe(foo(1,2)) or val |> foo(1,2)(^)
  • you can take functions already written for "normal" calling (such as any web platform API) and call them like pipe(x=>foo(1,2,x)) or val |> foo(1,2,^)

JavaScript's first functional operator.

Pipe isn't particular intended to be a higher-order functional operator. Some languages with it use it as such; these tend to be languages that already have heavy support for HOFP down to the syntax level (such as having functions be curried by default, as in F#), where it's a natural and obvious fit. Other langs, such as Elixir, do not - their pipe operators are still function-focused, but rely on "normal" functions rather than higher-order ones, because they just insert the piped value as an additional argument. Some languages do all of the above, like Closure, which has macros to handle piping in multiple different ways, including both HOFs and placeholders, according to the author's preference.

I pursued the pipe operator in its current form precisely because I felt that it deserved to serve more use-cases than just HOFs, because JS is not particularly oriented toward HOFP. (It allows it without too much difficulty, but lacks a lot of useful features that would make it more natural/convenient to use.) Libraries oriented strongly toward HOFP should still be able to benefit from it, as it's a generic linearization syntax that aids with lots of different code patterns, but they won't be getting maximal benefit, it's true. That's fine; the pipe() function will still exist and continue to be useful, just as much as all the other functional operators such a library is probably packing.

With the F# pipeline, any function that is currently pipeable with a standard functional pipe will "just work". Meaning zero migration for existing libraries to leverage F# pipeline OOTB.

Correct. But conversely, any function that's not currently pipeable (like every web platform API) will continue to be slightly inconvenient, meaning interacting with the rest of the web platform will continue to be slightly inconvenient. This is a "six of one, half dozen of the other" situation, where one usage pattern gets maximal convenience and other gets minor inconvenience. Figuring out the way forward is a matter of determining how to weight the two opposing classes of usage patterns so we can figure out which one can bear the burden of the minor inconvenience better. Obviously, my personal determination is that HOFs represent a lighter weight on the scale vs JS's operators, web platform APIs, and all non-HOF-based libraries. This is further influenced by the fact that HOF-based libraries already have a working and convenient pipe() function, while non-HOF-based libraries do not have a comparable linearization strategy allowed under current JS syntax without bizarre abuse of the comma operator.

(I understand that Typescript currently requires some ~shenanigans~ to make pipe() type properly, because it currently doesn't have full type unification and instead gets by with a more limited type inference strategy. It seems to be acceptable in practice, tho, and the TS team seems to be seriously looking into better inference to solve a multitude of HOFP problems similar to this.)

@tam-carre

This comment has been minimized.

Copy link

@tam-carre tam-carre commented Sep 9, 2021

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

Is there a reason why this option isn't being considered?

@theScottyJam

This comment has been minimized.

Copy link

@theScottyJam theScottyJam commented Sep 9, 2021

@tam-carre

It has been considered. This proposal has been around for a long time, lots of angles have been considered.

The issue with the partial application syntax is that it's very limited in its capabilities to avoid ambiguities (so no, F# + partial application will not please everyone) - you can read more about what it can and can not do in that proposal.

Here are some things that hack-style pipes can do, that partial application is simply incapable of helping out with. In other words, you would have to make a function literal in all of these scenarios if we were going with f#-style + partial application.

value |> foo(^, ^) // Using the same token twice means something different in hack-stile vs partial application
value |> f(^, g(^))
value |> ^ + 1
value |> [^, 0]
value |> {foo: ^}
value |> new f(^)
value |> `${^}`
value |> await ^
value |> (yield ^)
value |> import(^) // import() is a pseudo function, not a real one
@tam-carre

This comment has been minimized.

Copy link

@tam-carre tam-carre commented Sep 9, 2021

@theScottyJam

The usage stats for the comparison are still going to be quite significantly different if we are comparing the syntax tax for "unary functions (Hack) VS non-function-call expressions (F# with partial application)", and if we are comparing that for "unary functions (Hack) VS n+2-ary functions and non-function-call expressions (F#)", as this gist does -- which is why I'm asking why it is not currently being considered.

@sdegutis

This comment has been minimized.

Copy link

@sdegutis sdegutis commented Sep 9, 2021

value |> foo(^, ^) // Using the same token twice means something different in hack-stile vs partial application
value |> f(^, g(^))

I missed this in the proposal, and can't find it. What does each do?

@euan-smith

This comment has been minimized.

Copy link

@euan-smith euan-smith commented Sep 9, 2021

@sdegutis So the use of the partial application here is that foo(bar,?) returns a function which requires one parameter (where ? is the placeholder), so that you can do something like the hack-style with the F# proposal. In either case where one parameter of a function call is targetted val |> foo(bar,?) would do the same thing. However it does not work for a repeated parameter (or any other of the cases @theScottyJam raised above) - foo(?, bar, ?) will return a function which expects two parameters - the ? is a placeholder for any parameter, not a duplicated parameter, so it will not do the same thing as foo(^, bar, ^) in a hack-style pipe.

For the second example f(^, g(^)) would be turned into something equivalent to a=>f(a, b=>g(b) ), so the second parameter passed to the function f is a function which calls g, not the result of calling g.

@sdegutis

This comment has been minimized.

Copy link

@sdegutis sdegutis commented Sep 9, 2021

If I'm understanding you right, that's super confusing and unintuitive. I'd expect that in v |> foo(^, ^) it would assign const tmp = v and then just call foo(tmp, tmp).

@euan-smith

This comment has been minimized.

Copy link

@euan-smith euan-smith commented Sep 9, 2021

@sdegutis For the hack pipeline, you are absolutely correct. v \> foo(^, ^) would be transformed to a call to foo(v,v).

It is for the proposal to make hack-like syntax work by implementing the F# pipeline and the partial application which works differently (but looks the same).

FWIW I think the partial application proposal could cause a lot of confusion, at least initially.

@benlesh

This comment has been minimized.

Copy link

@benlesh benlesh commented Sep 9, 2021

@tabatkins thank you for your response. If you'd like to look, I have a discussion about what RxJS will need to do if the Hack Pipeline does indeed land in JavaScript it can be found here.

If you'd please indulge me on a little bit of history around RxJS and TC39 proposals:

Since I started working on RxJS ~7 years ago in version 5 (it was going to be 3 at the time, haha), has always had to goal to move towards leveraging standards. Originally we were targeting the observable proposal and we were very hopeful about the bind :: operator. Sadly, both stalled (and are seemingly dead).

RxJS, being a once straight-port of Rx NET by Microsoft, inherited a huge assortment of methods (aka "operators" in Rx-lingo). This was, in part, because it was coming from a compiled language that is easy to optimize, and also in part because of how many operations are possible with a type like observable. The problem in JavaScript, particularly when shipping to the browser, is obviously the size that adds. So the solution was to "detach" methods and hope for that :: bind operator in the long-term, while we used prototype-patching in the short term. Prototype patching came with a litany of problems (I'm sure you can speculate here, I won't dig into that), and with :: bind basically dead, we moved to a functionally piped approach, deeply hopeful that |> would come along and give us a standards-directed way to use the library.

In short, in an effort to try to keep moving towards proposals that seem to have a solid chance we've been continuously confounded over the years. If you sense any frustration on my part, that would be the origin.

At the end of the day, my goals, and I suspect the goals of the larger RxJS team (but I won't speak for them) center around trying to deliver the best DX we can to our users.

I've been talking with @jamiebuilds, whom I honestly consider a dear friend, and whom I've been a general dick to on Twitter recently (sorry, Jamie). Where would be the appropriate place to list all of the concerns I have about the Hack Pipeline proposal concisely so they can be addressed?


Unrelated, other major issues the RxJS community cares deeply about: cancellation, any general notification APIs, and scheduling. In particular cancellation.

@sdegutis

This comment has been minimized.

Copy link

@sdegutis sdegutis commented Sep 9, 2021

These discussions and debates are helpful for clarifying to JavaScript users, to help us understand what's at stake, what the options are, and how they'll help us change our way of coding. But how can we help to push the actual process along, or cast our votes, or in any way make this become a reality? I suppose the next natural step is to use each of these options via Babel and get traction going, so that the standards committees have real world usage to go based on. But when I looked into the Babel plugin I linked to above, I can't get that plugin working in the Babel playground, which makes me think it's unsupported and that the community has moved on to another similar (or superceding) plugin. It's difficult to tell where the state of the community consensus and Babel plugins currently are.

@sdegutis

This comment has been minimized.

Copy link

@sdegutis sdegutis commented Sep 9, 2021

(@benlesh I think we asked the same basic question at roughly the same time.)

I guess a more concrete question is:

  1. Are there currently forums or mailing lists where JS end users can discuss these points with JS decision makers?
  2. If not, can we start a forum that we all agree on, and elect a moderator, or something?
  3. GitHub Issues might be a good place, especially with 👍 buttons to reduce noise. Also it's free. Also pretty sure tc39 has one.
@ljharb

This comment has been minimized.

Copy link

@ljharb ljharb commented Sep 9, 2021

The bind operator isn’t dead; i expect a proposal to replace it to be on the next agenda. In concert with hack pipes, the bind operator and this-sensitive functions would likely work quite well together.

The locations to discuss proposals (that most people on this thread are well aware of) are a proposal’s repo, and https://es.discourse.group.

@sdegutis

This comment has been minimized.

Copy link

@sdegutis sdegutis commented Sep 9, 2021

According to a member of TC39 who I contacted, we can post these concerns directly in the TC39 repos. (I wasn't aware each proposal had their own repos.)

Which means we can post our questions and concerns in the Issues of https://github.com/tc39/proposal-pipeline-operator which is the Pipeline operator proposal, and includes all 3 options.

I'd suggest using social media to rally people to individual issues to comment or vote or try to request status updates or encourage progress or something.

@tabatkins

This comment has been minimized.

Copy link
Owner Author

@tabatkins tabatkins commented Sep 9, 2021

Just to get ahead of any confusion, I will note that "voting" is not a signal the committee uses for anything (besides occasionally an informal temperature check on an idea). Community discussion is great for making sure we get opinions and arguments that we hadn't considered, but volume is not a factor in itself. (And, because we're humans, can be an anti-factor, if a topic induces dread due to pile-ons.)

I've also closed the majority of syntax-related issue threads in the repository now that the proposal has advanced with a chosen syntax, but I'd suggest people review some of those, particularly the longer ones, to see what has already been discussed and considered. Quite a lot of ideas have been proposed and critiqued over the three years this proposal has been developed so far.

@kiprasmel

This comment has been minimized.

Copy link

@kiprasmel kiprasmel commented Sep 12, 2021

Please see tc39/proposal-pipeline-operator#91 (comment) and further discussion.

(context: the issue is for choosing which token to use for the Hack's operator, and people are struggling - I make a case that there's a reason behind it - F# + partial application > F# > no pipe operator > proposal 3 - Hack with |>> > other proposals)

Furthermore, currently the TC39 committee and their champions for the pipeline operators feel one-sided. Until we see equal representation for the F# proposal as we have for Hack from the committee (3 champions vs 0), I do not agree it was fair to reach Stage 2, let alone the possibility of moving to Stage 3.

@maxloh

This comment has been minimized.

Copy link

@maxloh maxloh commented Sep 12, 2021

Smart Mix seems great. Was it proposed to TC39?

@lightmare

This comment has been minimized.

Copy link

@lightmare lightmare commented Sep 12, 2021

Smart Mix seems great. Was it proposed to TC39?

Yes. The wiki points to a 2018 presentation showcasing it, and a 2021 presentation explaining that it was withdrawn in favour of Hack.

Until we see equal representation for the F# proposal as we have for Hack from the committee ...

You forgot Elixir. That one wasn't even mentioned in either of the linked presentations.

@mAAdhaTTah

This comment has been minimized.

Copy link

@mAAdhaTTah mAAdhaTTah commented Sep 12, 2021

@lightmare

You forgot Elixir. That one wasn't even mentioned in either of the linked presentations.

Elixir was considered and rejected because inserting an argument in the beginning of the argument list is very unlike JavaScript and would be confusing to developers.

@lightmare

This comment has been minimized.

Copy link

@lightmare lightmare commented Sep 12, 2021

Elixir was considered and rejected because inserting an argument in the beginning of the argument list is very unlike JavaScript and would be confusing to developers.

That argument is about as valid as "F# calling a function without any () or `` following it is very unlike JavaScript". They're different syntaxes for a new feature, both are unlike JavaScript in a sense: one injects an argument into a function call, the other conjures up a function call where there wasn't one.

But I didn't bring up Elixir to argue its merits here. I wanted to point out that when one demands equal representation, picking a favourite proposal (which was presented twice to the committee), while completely ignoring another (which wasn't presented at all), doesn't sound much equal.

@antmelnyk

This comment has been minimized.

Copy link

@antmelnyk antmelnyk commented Sep 16, 2021

Whatever goes closer to

pipe(
  getUser,
  doSomething,
  doElse
)

Is the best. It just fits JS and the existent JS FP world better. The bigger difference, the bigger increase in visual clutter, especially in more complicated cases.

F# looks cleanest and closest to the functional way (which is the whole point of adding the syntax... I assume). It just feels more natural to existent functional JS code, with fewer frictions when you need to extract something from a pipe to a separate function.

F# way embraces functions, Hack one tries to... hack them (pun intended) by introducing some weird symbols. Like in what world this would suit the language:

|> # + 1

Instead just using a function:

|> x => x + 1

Or even better:

const addOne = x => x + 1;
// ...
|> addOne

In general, though, seems like all discussions don't matter and TC will just go with what they like best. Borderline ignorance in many pipeline operator discussion threads is actually funny. The Promise/Monad history will repeat again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment