Show HN: A JavaScript function that looks and behaves like a pipe operator
github.com
github.com
pipe((p) => ([
"hello",
p.concat("!"),
send,
p.status,
console.log
]));
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.
pipeSync(p => [
"hello",
p + " world",
console.log
]); gulp.src(config.src)
.pipe(uglify())
.pipe(gulp.dest(config.dest))
.pipe(size());
[1] https://www.npmjs.com/package/gulp from functools import partial
class Pipeable:
def __init__(self, fn):
self.fn = fn
def __ror__(self, lhs):
return self.fn(lhs)
def pipeable(fn):
return lambda *args: Pipeable(partial(fn, *args))
filter = pipeable(filter)
map = pipeable(map)
list = pipeable(list)
sum = pipeable(sum)
min = pipeable(min)
max = pipeable(max)
any = pipeable(any)
# Usage:
range(1, 100) | filter(lambda x: x < 50) | max()
# 49
[1, 2, 3, 4] | filter(lambda x: x % 2 == 0) | map(lambda x: x * 3) | list()
# [6, 12]
[1, 2, 3, 4] | map(lambda x: x >= 5) | any()
# False Object.prototype.pipeTo = function(f) {
return f(this)
};
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. Object.prototype.pipeTo = Object.prototype[PIPE_TO]
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.
V(greeting, // initial value "hi"
V(capitalize), // custom function call "Hi"
V.concat("!"), // String method `concat` call "Hi!"
V(send), // custom async function call Promise { <pending> }
V.status, // automatic promise chaining + getting property Promise { 200 }
V(console.log), // automatic promise chaining + global function call logs 200
)
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! (-> x
(f)
(g)
(h))
https://clojure.org/guides/threading_macros (-> 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.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.
(->> (range 10) ;; (0 1 2 3 4 5 6 7 8 9)
(filter even?) ;; (0 2 4 6 8)
(map inc) ;; (1 3 5 7 9)
(apply +)) ;; 25
=> 25
which macroexpands to: (apply + (map inc (filter even? (range 10)))) status = greeting+"!" ~> capitalize ~> send
This would need 2 changes to JS:1: ~> being a pipe operator
2: Calling an async function from within an async function implies await
Without 2, it would look like this:
status = greeting+"!" ~> capitalize ~> await send a = d(c(b,7))
into a = b |> c(%,7) |> d(%)
and I would like to see it turn into a = b,7 ~> c ~> d a = (b, 7) |> c(*%) |> d(%)
Too much operator soup I guess. a = b |> c(...%, 7) |> d(%)
Spaciousness helps here a bit, but still way too many syntax, I agree. 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):
a = b |> c(7) |> d
a = d(c(7, b))
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 :-)
x ~> f = f x const result = (
await fetch(url)
.json()
::Object.keys
.map(key => ...)
::new ByteArray
);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![1] https://hackage.haskell.org/package/base-4.18.1.0/docs/Data-...
a = f((e(d(c(b))), 4), 5)
Seems not to make sense. Replacing c(b) with x we get: a = f((e(d(x)), 4), 5)
Replacing d(x) with x we get: a = f((e(x), 4), 5)
Replacing e(x) with x we get: a = f((x, 4), 5)
What is that? a = f(e(d(c(b))), 4), 5)
b is passed to c, then the result is passed to d, then to e along with 4 as a second parameter, and finally to f along with 5. a = f(e(d(c(b))), 4), 5)
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 b ~> c ~> d,4 ~> e
This reads as b ~> c ~> (d, 4) ~> e
to me. Sure, you can get used to it, but it’s pretty unnatural. Is this syntax used in any other languages?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.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'''
greeting = capitalize(greeting);
greeting = greeting.concat("!");
greeting = await send(greeting);
greeting = greeting.status;
console.log(greeting);
but with less accurate breakpoints?It has a lot of interesting features that would be difficult to replicate in JavaScript.
[0] https://wiki.tcl-lang.org/page/pipethread
[1] http://www.rkeene.org/tmp/pipethread-presentation-withnotes....
export const pipe = <A, B>(f: Fun<A, B>) => {
return {
to: <C>(g: Fun<B, C>) => pipe((arg: A) => g(f(arg))),
build: () => f,
};
};
Much simpler alternative. const process = pipe(readLines).to(x => cut(x, 2)).to(....).build()
const result = await process(fd)
P.S the Fun type is just (...args: A) => Bhttps://github.com/WiseLibs/wise-river
For complex uses
pipe([
f,
g,
h,
])(123);
Or, you know, a functional language.Same thing. Less code, standard functions.
And then, remembering that everything has already been invented, I found this:
https://github.com/elixirscript/elixirscript
You're welcome.
const { status } = await
send(
capitalize(greeting)
+ "!"
)
But i would not concat things in a method call, bad style. const message = capitalize(greeting) + "!"
const { status } = await send(message)
The example from the page is a little bit too simple.With 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.
Looks tidier to my eyes
const newPipe = ppipe.extend({
divide (x, y) {
return x / y;
},
log(...params) {
console.log(...params);
return params[params.length - 1];
}
});
const res = await newPipe(10)
.pipe(x => x + 1)
.divide(_, 11)
.log("here is our x: ") //logs "here is our x: 1"
.pipe(x => x + 1) // 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!https://gcanti.github.io/fp-ts/modules/function.ts.html#pipe
pipe(
2,
double,
x => x.toString()
)
I love your approach too, it's really more of a pipe operator than a `pipe` function which expects the pipeline to happen on the arguments side. curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/docker.gpg \
> /dev/nullHowever, 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.
V(greeting, capitalize, ['concat', "!"], send, 'status', console.log )
but at that point it's already Ramda or smt and doesn't look as funky
>const { status } = await send(capitalize(greeting) + "!")
>console.log(status)
No it's not hard to read. >Make it less nested, more vertical, by using the V "pipe":
>V( greeting, // initial value "hi"
>V (capitalize), // custom function call "Hi"
>V .concat("!"), // String method `concat` call "Hi!"
>V (send), // custom async function call Promise { <pending> }
>V .status, // automatic promise chaining + getting property Promise {200}
>V (console.log), // automatic promise chaining + global function call logs 200 )
This cursed abomination is a joke isn't it?The pipe operator is just one part of functional programming, composing and applying functions
`f(g(x))` is still functional, and I would argue it exactly what is happening in the first example.
That you don't recognise that is strange to me, and if after my explanation you can't understand that, I don't know what to tell you.
It's not a problem, don't worry about it you will get there. Enjoy your day!
The following example code is a bit hard to read:
const { status } = await send(capitalize(greeting) + "!")
console.log(status)
I disagree, I find this example code very easy to read because it reads like idiomatic Javascript. Unlike this library.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
2.chain(x => x * 2).chain(f)
instead of f(2*x)
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!
I love JavaScript but this pipe thing is horrific.
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.
const msg = capitalize(greeting) + “!”;
const { status } = await send(msg);
console.log(status);
Now it’s perfectly readable, no weird shenanigans required.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...
const foo = capitalize(underline(reverse(exclaim(indent(value)))));
I already forgot the amount of parentheses I needed to close with while typing that.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.
value.indent()
.exclaim()
.reverse()
.underline()
.capitalize()
There’s always the OOP version of method chaining!I think it could be done today with a (big) number of ESLint plugins.
At first the constraints may be difficult to grasp. You may feel like a feature is missing, but that's part of learning a language. Once you have learned them, the constraints actually become helpful.
One thing I dislike about JS is that it's like the wild west. You have a mash-up of all different paradigms that are possible, so you get libraries like this. No, I wouldn't say this makes code more readable, though I'm a fan of the pipe in other languages. It just makes yet another way to do the same thing.
I don't mean to be rude or ranty, so with that said, it's cool that you were able to make this. Kudos for that.
That's interesting. I'm pretty well convinced of the opposite. I primarily work with Ruby, which doesn't really have a whole lot of language constraints in the context of "what can I do?". Part of the language design philosophy of Ruby is to give developers "sharp knives" and let them individually avoid chopping off their own fingers. I haven't tried my self but I would expect that something similar or identical to this could be implemented in Ruby and I wouldn't consider that to be something which makes it worse. Indeed, that's one of my favorite features of the language.