Nice idea, there is also @babel/plugin-proposal-pipeline-operator which would require much less changes in the code if pipeline operator is introduced in the future. But it requires working babel.
I think the implication is that it's difficult to read because it's read non-linearly. You first need to start in the middle at `greeting`, then work your way out to `capitalize` on the left, then to `+ "!"` on the right, then work your way out one more layer to the left with `send`.
Some kind of pipe operator can make this easier to read by arranging the operations linearly from left to right (or top to bottom as is the case with this library). Although in my opinion this library has its own readability issues.
Right - because this library tries to fake a new language syntax feature without the actual support for it, it ends up being the worst of both worlds. Even ignoring the lines-of-code explosion, the non-standard use of parens/spacing makes it incredible difficult for me to parse.
Props to the author for trying out something neat, but this is probably a bit to clever for my liking.
Author here. Thanks for your nice comment. I added a note to say it does work with autoformatters too. Actually someone in a comment below said he or she prefers the autoformatted syntax.
Oh, that's good to know! I haven't tried it and assumed an auto formatter might misalign some of it or put things on the same line, so the visual "downward arrow" appearance would be lost
Edit: I just checked the docs again and while it does look a bit "misaligned" at the start, I do think it looks more readable with the autoformatted version. Pretty cool!
Ahh OK. Yeah, I always find trying to shoehorn language semantics via functions to be kinda awkward. The best attempt I've seen is this addition of async/await to Lua, which actually looks pretty usable [1].
Is it? I think it's an implementation of the pipe operator rather than satire of it. Javascript doesn't provide the flexibility to extend the language with pipe operators (though tools like Babel will let you use the pipe operator already).
The clunky syntax removes all the benefits of the pipe operator. The entire thing is syntactic sugar, wrapping it in confusing functions just breaks the entire concept.
Still not hard: In that case, instead of msg use sendMsg AND/OR in the second statement use { status, msg: resultMsg }, or just result and later result.msg.
Because they provide value and document the chain of events. I fear a long list of piped expressions without any idea of what they are intended to to. Would be like big regexps.
`greetingMsg`?
`greeting`?
Naming the other one differently?
Splitting the work done with that message and the work done with this message into their own functions?
Frankly, this always sounded like a readability issue seeking a readability problem. Splitting because the names clash, because you decided to name things, because a one-liner was less than ideally readable.
Code is read many more times than it is written. Put in the minute or two to devise good intermediate variable names, and that will pay back manyfold every time someone needs to read and understand it.
Some code. E.g. most of my code was barely read by anyone, as most of it was utilitary or mvp or niche/local enough to never see big light. And when from time to time I read other code, e.g. on github, in projects I’m occasionally interested in, there’s barely anything resembling these high class advices. FOSS with large reach - yeah. But a library that you found for a specific hardware is “just code”. And to be honest, the difference between the two is not that big. Any decent programmer navigates both almost equally fast. As someone who modified Lua internals (typical example [1]) for employer’s needs I can say that I prefer Lua style to ceremony on top of ceremony, to multiparagraph explanations of what should be obvious to a competent developer, and to hundreds of extracted functions for the sole reason of clean-ish scopes or whatever.
But still I’ve done what people advised, for many years. And know what? It never paid back. I’m sort of tired to keep that secret and nod to this obvious overgeneralization and a blanket idea. Pretty sure I’m not alone.
This is clean JavaScript syntax in my opinion and should be what people strive for. It's perfectly readable, it's faster, it does async correctly without any unnecessary computation, can be typed and will have a normal stack trace. Piping is cool when done right, but can introduce complexity fast. Elixir is a good example where it works wonderfully.
This won't look as good when you include all the boilerplate which defines those methods, and which needs to be repeated or otherwise included for every "value" type. You don't have to worry about this when they are pure utility functions.
Which is great until Prettier, in its infinite wisdom, decides it's better to stick four of these function invocations on one line and two on the next, so you have an obnoxious mishmash of tokens!
Which is a problem of Prettier. I wish for the day that someone builds rustfmt but for Typescript, I will happily leave Prettier and StandardJS behind.
I think it could be done today with a (big) number of ESLint plugins.
It's certainly a nice usage of JavaScript's Proxy object and the resulting syntax is simple to grasp. Also glad to see the TS definitions in there. Well done!
And call it a day. It does about 80% of what a pipe macro would do, has instant compatibility with other prototype methods and leads to very simple types. The only issue is that you have to define lambdas to call multivariable functions. D has got this very, very right.
or some other name instead of `pipeTo` should there be a conflict.
JS already has a few well-known symbols like Symbol.toPrimitive that allow one to modify an object's behaviour (in this case what happens when `valueOf` or `toString` is called), so there's precedent.
It feels a bit unnatural to pipe multiple arguments like that in JS, and you could inline them in the first call without losing legibility:
a = c(b, 7) |> d(%)
It's the arguments added way down the line that are problematic:
a = f((e(d(c(b))), 4), 5)
a = ((b ~> c ~> d), 4 ~> e), 5 ~> f
a = b |> c(%) |> d(%) |> e(%, 4) |> f(%, 5)
Smart pipes [1] were a bit nicer about that:
a = b |> c |> d |> e(#, 4) |> f(#, 5)
Basically you would use # token whenever you wanted an expression and just a function name when you needed to pass a single argument. Best of both worlds IMO.
Perhaps it could even be extended for Curry-ish function syntax (although it isn't as obvious):
> P. S. When studying Haskell on a CS course in school, I used to define exactly the same squiggly arrow operator for my programs :-)
Haskell base already has the reverse application operator (&) which does the same thing. Not included in the prelude but can be imported from Data.Function. [1] It's defined as:
(&) :: a -> (a -> b) -> b
x & f = f x
Not that there's anything wrong with using your own definitions, just thought I'd point it out!
That has 4 opening parenthesis and 5 closing parenthesis.
b is passed to c
b ~> c
then the result is passed to d
b ~> c ~> d
then to e along with 4 as a second parameter
b ~> c ~> d,4 ~> e
and finally to f along with 5
b ~> c ~> d,4 ~> e,5 ~> f
The nice aspect of "this ~> that" is that the meaning is simply "put this into that". And that it results in the shortest code.
An alternative would be:
b ~> c ~> d ~> e(%,4)
Slightly longer, but maybe easier to read. Also easier to handle, as you can remove the fourth part " ~> e(%,4)" without having to change the third part.
If you are interested in this kind of things, also have a look at fp-ts and it’s version of pipe and flow. Also comes with various monads to play around with!
I mean, if you abuse formatting enough I suppose... although I will say, the pipeline/bind operator has been in proposal for a decade at this point.. it's depressing that some version hasn't shipped, because C# extensions and similar in swift can really help shift how from what.. it* could really help JavaScript a lot
But then you have to define each method in advance, it doesn’t look like the pipeline operator at all, but more like the old jQuery.fn.method shenanigans.
For those that don't know. In Clojure it is also perfectly valid to drop the parantheses for each subsequent call in a threading macro if you don't want to pass in any additional arguments:
(-> x f g h)
If you need to pass an additional argument to g you can always do:
(-> x f (g foo) h)
There are thread first ->, thread last ->>, and even a thread as "as->" depending on where you want to place the argument when you pipe the result through the thread of functions.
I like goofy projects like this one but I think that if you insist on having calls there, then might as well make it more explicit with something like this:
Where `p` stands for Proxy to the previous result. Under the hood it would just return an object describing the name of the called method and arguments passed.
The pipe function would then iterate over the array, calling methods as described.
It's not immediately clear what should happen when a method's return type is a function, but I suppose this can be handled via convention.
I've used the Remeda package to preserve type information in pipes in Typescript: https://remedajs.com/. There may be better implementations (fp-ts?), but I like Remeda's docs.
I don’t like the syntax . But pipe operator concept is great for helping you think about your program as a series of data transformation steps . I just write my simple pipe function and use it . No need for library .
The "vertical" example looks contrived in JavaScript, but if that's your thing, I think it's OK.
But please don't promote antipatterns from languages that actually have a pipe operator (like Elixir) such as the '''concat("!")''' where the previous implementation's version using an operator is much clearer and more idiomatic.
I see a ton of Elixir code where people shoe-horn things into a vertical pipeline where the "normal" code would be a lot more readable, for example invoking '''Kernel.+(2)''' instead of just doing '''my_var + 2'''
const { status } = await send(capitalize(greeting) + "!")
console.log(status)
I find that easy to read, and your pipe operator harder (but still sensible!). I guess that just reflects the backgrounds we come from, people from more functional backgrounds (maybe lisp or Haskell) will find the latter example easier I guess!
The pipe function of `fp-ts` doesn’t have direct access to methods and properties of the pipe output like Verticalize. You have to wrap them into anonymous functions. Same with promises.
Yeah, I did something similar in effect in Python some time ago: https://github.com/vollcheck/pypelines - this is a bytecode hack rather than operator overloading.
This solution seems a lot cleaner to me syntactically, but it does lack helpers like .concat(). You'd have to write methods like .pipe( x => x + "!") to extend methods. Then again, prefixing and postfixing operaties shouldn't be too hard to write either
147 comments
[ 4.8 ms ] story [ 348 ms ] threadWith complex operations a pipe-method would make more sense.
But i will wait for the native pipeline-operator <https://github.com/tc39/proposal-pipeline-operator> instead using now a function.
const { status } = await send(`${capitalize(greeting)}!`)
console.log(status)
It does reduce the number of discrete operations to make the pipe example look more impressive, though.
And let the compiler optimize your code, that is the job of a compiler. Write your code for humans.
Some kind of pipe operator can make this easier to read by arranging the operations linearly from left to right (or top to bottom as is the case with this library). Although in my opinion this library has its own readability issues.
Sure, it is good most of the time, but operator precedence rules exist for a reason.
I don't want to read
instead of These patterns have their place for async programming, pure FP, streams and more.They can also be a distraction.
E.g. the library introduces its own promise-unwrapping semantics and more.
I don't see the use for such a generic implementation, although I applaud the effort.
Props to the author for trying out something neat, but this is probably a bit to clever for my liking.
Same here. Not to mention this wouldn't work in a codebase with an autoformatter.The idea/API is clever. So, a pretty cool experiment I suppose
Edit: I just checked the docs again and while it does look a bit "misaligned" at the start, I do think it looks more readable with the autoformatted version. Pretty cool!
https://github.com/tc39/proposal-pipeline-operator
I love JavaScript but this pipe thing is horrific.
[1] https://github.com/ms-jpq/lua-async-await
The clunky syntax removes all the benefits of the pipe operator. The entire thing is syntactic sugar, wrapping it in confusing functions just breaks the entire concept.
I’m obviously not being serious about naming being hard in this example.
But there are cases where the pattern of using return object property names as variables comes back to bite uou.
Even in this case, what if the response object from ‘send()’ also has a ‘msg’ property?
Changing const { status } into const { status, msg } is an error.
We should be cautious about syntaxes that force us to allocate names. Sometimes.
You don't have to write something like "this is the message", you can see it.
But still I’ve done what people advised, for many years. And know what? It never paid back. I’m sort of tired to keep that secret and nod to this obvious overgeneralization and a blanket idea. Pretty sure I’m not alone.
[1] https://github.com/lua/lua/blob/6baee9ef9d5657ab582c8a4b9f88...
Better that you either avoid that pattern or at least indent it in such a way that its not so visually confusing. Or yknow use a pipe operator.
I think it could be done today with a (big) number of ESLint plugins.
However, in R, you don't need to trailing slashes on new lines. Plus, their pipes are hideous: %>% or now |>.
I do kind of like how the pipe delimiter looks on the left.
JS already has a few well-known symbols like Symbol.toPrimitive that allow one to modify an object's behaviour (in this case what happens when `valueOf` or `toString` is called), so there's precedent.
https://github.com/mlajtos/es1995
1: ~> being a pipe operator
2: Calling an async function from within an async function implies await
Without 2, it would look like this:
https://github.com/tc39/proposal-pipeline-operator
Perhaps it could even be extended for Curry-ish function syntax (although it isn't as obvious):
Too bad it was withdrawn.[1]: https://github.com/tc39/proposal-smart-pipelines
P. S. When studying Haskell on a CS course in school, I used to define exactly the same squiggly arrow operator for my programs :-)
Haskell base already has the reverse application operator (&) which does the same thing. Not included in the prelude but can be imported from Data.Function. [1] It's defined as:
Not that there's anything wrong with using your own definitions, just thought I'd point it out![1] https://hackage.haskell.org/package/base-4.18.1.0/docs/Data-...
An alternative would be:
Slightly longer, but maybe easier to read. Also easier to handle, as you can remove the fourth part " ~> e(%,4)" without having to change the third part.[1] https://github.com/gcanti/fp-ts
Looks tidier to my eyes
The pipe function would then iterate over the array, calling methods as described.
It's not immediately clear what should happen when a method's return type is a function, but I suppose this can be handled via convention.
https://github.com/Tade0/pipe/blob/master/pipe-sync.js
See Clojure macros for thread-first `->`, thread-last `->>`, thread-as `as->`, `some->`, `some->>` and `cond->`: https://clojure.org/guides/threading_macros
You can leave all this hurt behind you and use ClojureScript, which compiles to JavaScript.
But please don't promote antipatterns from languages that actually have a pipe operator (like Elixir) such as the '''concat("!")''' where the previous implementation's version using an operator is much clearer and more idiomatic.
I see a ton of Elixir code where people shoe-horn things into a vertical pipeline where the "normal" code would be a lot more readable, for example invoking '''Kernel.+(2)''' instead of just doing '''my_var + 2'''
https://gcanti.github.io/fp-ts/modules/function.ts.html#pipe
https://github.com/WiseLibs/wise-river
For complex uses