5 ms·
The thing about the pipeline operator that strikes me is how toylike all the examples are. Look at this article for example - it’s a bunch of examples of adding
by johnfn 5y ago
The thing about the pipeline operator that strikes me is how toylike all the examples are. Look at this article for example - it’s a bunch of examples of adding and exponentiating numbers, as if you couldn’t do that already in a single line of code.
My suspicion is that once you move away from toy examples, most pipeline operator use cases are already covered by chaining class methods on classes. I suppose you could argue that the pipeline operator allows you to operate across multiple different data types, but I further suspect that handling many different data types with a single function is an annoying thing to get right and this won’t come up in practice all too often.
To be fair, the case of pulling in only the methods you need on rxjs seems somewhat useful… but do we really need a whole new operator just for that? And isn’t that the point of ::?
- Smaug123 5y agoIt's a mindset thing. The pipeline operator encourages you to think about repeatedly transforming static dumb data. Dot-operators encourage you to think about repeatedly asking some smart blob of state to mutate itself. So the pipeline lends itself well to a purer mindset. F# itself much prefers the pipeline, because of how its type inference algorithm works. F#'s type inference finds it much harder to work out the types when you use a dot operator, because so many possible types T could have `(x : T).Foo(bar)`, all those types are unrelated to each other, and notions of "has a member `Foo`" are quite hard to get right in the type system. By contrast, if you call `x |> List.map bar`, the types are almost totally fixed in an easy-to-understand way. "Handling many different data types with a single function is an annoying thing to get right" - no, this is very common, and generics even make it easy.
- adz5a 5y agoDot operators vs plain functions are just two different ways to call a function, binding the lexical context `this` to a different value, possibly `undefined` in a strict context (which is default in es6 modules). The two are not uncompatible and libraries like jQuery make a wonderful use of the first version (dot call). The fact is that everything is mutable in JavaScript and one of its main API (the DOM) is actually built around this concept. I really fail to see how those kind of changes bring anything of value to the language, apart from creating complexity where there is much already: some codebases will try and use this before it's ironed out with different / changing / competing transpiler implementation while it is only stage 2. What happened with decorators (see babel-legacy-decorators) should be a warning against enthusiasm for these kind of things and remind me of the pitfalls of macros in the Lisp world... I am not saying that you should stick with an idiom till the end of time but changes should try and lower that complexity while (in the case of EcmaScript) being backward compatible so as to not break existing stuff. Btw I love RxJS and its variants, Ben Lesh's talks (as well as those by Matthew Podwysocki, Andre Staltz etc...) from around 2015 and then on were one of the reasons I got into JS as a professional.
- Smaug123 5y agoI merely explain where the pipeline operator came from and why it's preferred in F#: because it plays nicely with immutable data and the Hindley-Milner type inference algorithm. I make absolutely no comment about the sanity of adding anything to JS (or, indeed, of using JS at all).
- the_duke 5y agoChained function calls only work if the types in question have immutable functions that support chaining. This is the case for arrays with `map`, `filter` and for promises with then/catch. But it's not the case for the majority of libraries, including many of the builtin types. A pipeline operator is generic and can be used everywhere, without requiring the API to be built around chaining. This can often lead to much nicer, more readable code. I think this is something you only start to really appreciate once you've used it a bit, like in Elixir or F#. Obviously this is much more natural in a functional language, where everything is immutable anyway.
- slaymaker1907 5y agoThe pipe operator/function is useful and appropriate when all you are doing is stringing along some data through a series of transformations and where introducing a named variable would be more confusing and take away from the context of what is actually important. Methods are nice but suffer from the fact that adding a custom method to a new object is more involved and dangerous than just creating a new function. However, "f(g(h(x)))" is not very readable (at least for English speakers) since while we read left to right, the invocation order is right to left. "x | h | g | f" is arguably more readable since the evaluation order follows reading order (plus you don't have to deal with so many parenthesis). I want to reemphasize though that introducing a variable is often better practice and that the pipe operator only makes sense when there is no suitable variable name.
- Zababa 5y ago> However, "f(g(h(x)))" is not very readable (at least for English speakers) since while we read left to right, the invocation order is right to left. "x | h | g | f" is arguably more readable since the evaluation order follows reading order (plus you don't have to deal with so many parenthesis). There's also a "hierarchy" thing at play. With "f(g(h(x)))", x is "inside" h which is "inside" g which is "inside" f. With "x | h | g | f" all are on the same level. For me at least, it's easier to imagine your data "flow" through functions when they are on the same level. It sound a bit weird when I put it that way, but I don't think I'm alone in feeling that. Fluent interfaces are considered easy to read, and help flatten the code. Same thing with promise chaining compared to the callback > of doom.
- LAC-Tech 5y ago> Dot-operators encourage you to think about repeatedly asking some smart blob of state to mutate itself. Smart Blobs don't have to mutate themselves anymore than procedures have to have side effects.
- Smaug123 5y agoOf course they don't have to. As I said, they encourage you to think in the object-orientation "encapsulate state and teach it how to mutate itself on demand" way, but you don't have to use them that way.
- LAC-Tech 5y agoI'm being somewhat obtuse here, but I've never understood that. Why would I be more likely to mutate state in sending a message to an object than I would be applying a procedure to a struct? (or a function to a record, depending on your terminology) Immutable objects are a very old and mainstream idea, I believe they're even talked about in Effective C++.
- Smaug123 5y agoLong history and baggage of object orientation, I suppose. There's nothing inherently mutable about dot-notation (indeed, F# uses it for accessors on immutable records as well as for "proper" object-orientation), but lots of object-oriented languages use the dot for accessing objects, and lots of object-oriented idioms involve mutation. If you're going to send a message to an object, you do kind of expect something to happen, and that's often a mutation somewhere. Having said that, I actually don't know the history of the dot operator in any depth (https://cs.stackexchange.com/questions/89031/what-is-the-origin-of-dot-notation https://cs.stackexchange.com/questions/89031/what-is-the-ori... suggests it arose in Simula).
- Zababa 5y ago> Why would I be more likely to mutate state in sending a message to an object than I would be applying a procedure to a struct? I think most people don't think about objects in terms of sending a message to them, but in terms of calling a method on them. OOP in JS, currently, revolves a lot around mutable objects. JS itself revolves a lot around mutability. Some people are trying to change that, at least by getting proper support for the alternative. The pipeline operator is part of that.
- Zababa 5y agoI think one of the use cases is that it's easier to have a series of functions on immutable data compared to chaining classes. Considering JavaScript may get immutable data structures at some point, this would work well with them. There is also the fact that JavaScript supports different types of programming, functional being one of them. Having a pipe operator helps you to make very clean and readable functional code. Edit: the article also mentionned that methods can't be tree shaken. That's another improvement.
- graftak 5y agoTuples and records are proposed immutable data structures and currently in stage 2[1] (same as the pipe operator). [1] https://github.com/tc39/proposal-record-tuple https://github.com/tc39/proposal-record-tuple
- azangru 5y ago> once you move away from toy examples, most pipeline operator use cases are already covered by chaining class methods on classes But what if we don't want to use classes? What if we want to be cool and compose functions out of functions, Ramda-style, or Elm-style?
- nsonha 5y agoUnpopular opinion: we could just add, say, `pipe` and `pipeAsync` into the standard library, appart from aesthetic, I don't see the pipe operator to be very useful, at least the F# one doesn't because it's literally just another syntax for function invocation.
- azangru 5y agoStandard library? In javascript? You mean a keyword like `async` or `yield`, or a built-in object like Date or Math?
- pygy_ 5y agoWhen you look at a Ramda-based JS code base, all there is to look at is function calls. Such code eschews most statements. Piping is a common operation, though, and having dedicated syntax makes the code more readable. For those code bases that use curried, unary functions, the F# proposal is perfect (this is the original motivation behind the operator). The hack-style pipe syntax that has been chosen is much worse in that regard, which is disappointing for the very folks that requested its addition.
- slaymaker1907 5y agoIt is useful as an operator due to the fact that you avoid unnecessary function calls/lambda creation. "pipe(x, f, g, y => h(42, y))" is not going to be as easy to optimize as "h(42, g(f(x))" yet "x |> f |> g |> h(42, ^)" has the ordering of the pipe function while being equivalent to normal function invocation for the purposes of optimization. I sort of agree though about the F# proposal version. It doesn't make sense to introduce if it is basically equivalent to having a pipe/pipeAsync function. I suspect this is a big reason why the Hack version was favored.
- akra 5y ago> Chaining methods on classes But defining these is typically left to library authors in many languages. Writing your own Fluent/chaining interface isn't usually worth the effort. Where in these languages because any function is "chainable" and gets it for free a lot of programming ends up being chained pipelines. Anything where there is a logical workflow (a lot of programming) ends up being nice to read when expressed with pipes (i.e. do this, then this, then that, and finally return this). The pipe operator goes hand in hand with currying as well since you can complete some of the arguments before wiring the partial completed function into a generic workflow; a pattern that typically isn't be done with Fluent/Chaining methods on classes.
- overton 5y agoThe primary usecase (which happens to me fairly frequently in Javascript) is when you need to operate on a native or library data structure with some function other than its built-in methods. Some people also find it easier to program in an FP style (separating data and functions) in their app code. It seems to be a matter of neurodiversity.