Interesting read. I quite like the pipe operator as I think it would make a lot of code easier to read.
I would caution people against using some of these "features" though as writing clever code that leverages obscure techniques is NOT good for readability and maintainability.
The person who has to maintain your code will probably be able to more easily grok some boring if else statements than some weird ternary thing that leverages the comma operator.
Not that I necessarily disagree, but one counter argument would be "why even use the pipe method? You could just say
square(increment(square(2)))
and get the same result, right?"
The point is readability. It's UX for the programmer, reducing cognitive load in understanding what's going on. Pipes are fantastic in the terminal and have never been replaced there because they are very simple glue piecing together the complex bits with a simple-to-understand model.
If we can have that in JS, I'd be quite pleased even if there are other ways to do the same thing.
The advantage of the operator and the `pipe` helper over regular function composition is that you can emphasize the flow of data through a pipeline of functions.
The only advantage in readability I see here is that the operator is hard to mistake for anything else, it sticks out of the code, more than a seemingly regular function call. But this is not an advantage that justifies introducing the operator.
Other than that, UX and cognitive load is the same.
Pipes can indeed be fantastic! ;)
*
Apropos composition: why not add a composition operator while we're at it? Or we could define something like:
For me there is also something more ineffable that's important here. A question of whether adding a new built-in operator fits within the general philosophy or some sort of "look & feel" of the language, or whatever. I think JavaScript seems to be quite conservative, when it comes to bringing in new operators. Perhaps that's a good thing. Maybe not. But so far, I remain unconvinced.
Fuzzy words and your opinon, so can't exactly argue with that.
But here's a try ;)
I take it that the meaning behind the words you used is:
clean:
no need for `pipe(` prefix and `)` postfix
expressive:
can convey a pipeline in a distinctive and unique way
(with a special operator as opposed to regular function)
Let me use the same words, but with different meaning, to say "the pipe function is much cleaner and expressive IMO".
The meaning would be:
clean:
looks (is) the same as regular function application,
no need for dirtying the syntax up with
a special operator
need to only press one extra key per operand/argument
(,) as opposed to 3 (shift+|,>) ;)
expressive:
can convey a pipeline just as well as the operator;
it's a regular function, so it's first class,
unlike the operator
it's variadic, so you can combine it with
spread operator syntax like:
pipe(data, ...pipeline1, ...pipeline2)
// where pipeline1 and pipeline2
// are arrays of functions
On readability, all the code samples here are using pipe operators spread out on one line, but for anything nontrivial I think the point is to use it thusly:
Yeah readability is largely about what you're used to. I find the nested function calls more readable, as I've used that notation for decades going all the way back to high school math.
Imho, languages that break the abribtrary difference between functions and methods do this best, so there's no difference between Square(myNum) and myNum.Square(), since .Square() works nice for pipe-style usage.
Is there a good way to see the intermediate results at each stage in the pipe when things go wrong? Pipes are concise but perhaps harder to debug depending on what tools you have.
With the above, I can add in a breakpoint and inspect what's going on at each stage with access to other debugging tools. Log statements don't seem nearly as good as this. The above is nice as well in that you can give intuitive names to the intermediate results.
If you need names for intermediate results, etc. then you don't want the pipeline operator/function. In such cases the things you say are true. But when you wan't to be more concise, with the operator/function, you can.
BTW A nice feature of a code editor would be if you could magically switch back and forth between code with intermediate named variables and pipelined version of the same code, e.g. by selecting the code and invoking an option.
And a native operator can be optimized just about the same amount as a built-in function. So maybe add the function instead of the operator to the language?
One issue with a `pipe` function is that it is an arity of N. That makes it almost impossible to optimize for all the edge cases. In contrast, the pipe operator is always fixed at an arity of 2. Implementing a static Function.pipe in terms of the primitive operator becomes easy enough. Functional languages have played with both the function and the operator, but they keep coming back to the operator because it's more readable.
//implementation of your pipe in terms of the (potential) pipe operator
Function.pipe = (fn, ...args) => {
//let's use a pseudo Duff's device
switch (args.length) {
case 0: return fn;
case 1: return fn |> args[0];
case 2: return fn |> args[0] |> args[1];
case 3: return fn |> args[0] |> args[1] |> args[2];
case 4: return fn |> args[0] |> args[1] |> args[2] |> args[3];
case 5: return fn |> args[0] |> args[1] |> args[2] |> args[3] |> args[4];
case 6: return fn |> args[0] |> args[1] |> args[2] |> args[3] |> args[4] |> args[5];
case 7: return fn |> args[0] |> args[1] |> args[2] |> args[3] |> args[4] |> args[5] |> args[6];
case 8: return fn |> args[0] |> args[1] |> args[2] |> args[3] |> args[4] |> args[5] |> args[6] |> args[7];
default:
var mod = args.length >> 3; //div by 8
var rem = -8 * mod + args.length; //mult add
fn = Function.pipe(fn, ...args.slice(0, rem));
while (rem < args.length) {
fn = fn |> args[rem++] |> args[rem++] |> args[rem++] |> args[rem++] |> args[rem++] |> args[rem++] |> args[rem++] |> args[rem++];
}
return fn;
}
};
It's a little off topic, but I'd love to see the addition of well-known symbols for all the operators to allow custom overloading. Lua does operator overloading with metatables. No reason JS can't too. At that point, pipe, compose, and even spaceship become much more useful.
var x = {
foo: [1,2,3],
[Symbol.add](rightHandSide) {//Binary
return this.foo.concat(rightHandSide);
},
[Symbol.incr]() {//Unary
this.foo = this.foo.map(x => x += 1);
return this.foo;
}
}
//default to standard operator if
//if left side is primitive or if
//left side has no matching Symbol
x + 3; //same as x[Symbol.add](3)
[0].concat(x++); //=> [0,1,2,3]
//same as [0].concat(x); x[Symbol.incr]();
[0, 1].concat(++x); //=> [0, 1, 2, 3, 4]
//same as x[Symbol.incr](); [0, 1].concat(x);
You can also replace all `a |> b` with a fixed arity `pipe2(a, b)` in your code and again, no new operator needed. Anwyay, I don't think such optimization issues are enough to justify adding this operator to JavaScript. In other functional languages it may make sense, because operators work differently there (e.g. they are interchangeable with functions, there is support for overloading, etc.).
> I'd love to see the addition of well-known symbols for all the operators to allow custom overloading.
Sure, if we had support for first-class overloadable operators then a proposal to add a standard operator like `|>` would make sense. But we don't, so a better one would be to add `Function.pipe` (simple, fits into the language better) or allow operator overloading, etc. (also may be reasonable, but it's a major change in the language).
A small self-promotion, given the relevant context: I was frustrated with the lack of support for the pipeline operator and came up with this: https://github.com/egeozcan/ppipe - I currently use it on a couple of projects and it really helped make my code more understandable.
I would caution people against using some of these "features" though as writing clever code that leverages obscure techniques is NOT good for readability and maintainability.
The person who has to maintain your code will probably be able to more easily grok some boring if else statements than some weird ternary thing that leverages the comma operator.
Just because you can doesn't mean you should.