2018-01-25
This post was originally published here.
Putting operands/arguments before the operator/function is sometimes
very useful. Some functional languages have pipeline operators like
|>
for that. It’s been proposed to throw one into
JavaScript too. Seems like a good idea. Turns out maybe not quite, since
JavaScript is not like the other languages.
A better idea, I argue, would be to build in a mostly equivalent
pipe
function instead, which would fit better into JS.
Or full-ass the existing proposal by first (or also) changing how operators work in JS.
Or if you people really must have this operator RIGHT NOW, because
your personal happiness depends on it, at least throw in
pipe
along with it, so me and some like-minded folks can be
content too.
I was inspired to write this, because of a discussion I recently had on Hacker News and a few comments on topic some time ago on reddit. This post compiles and expands upon my and others’ arguments.
So a lot of people seem very excited about
this
proposal to introduce a pipeline operator (|>
),
popular among functional languages such as F#, Elm, Elixir or Ocaml, to
JavaScript.
Firefox
has it even implemented, hidden behind an
–enable-pipeline-operator
compile flag.
The operator is supposed to be “a backwards-compatible way of streamlining chained function calls in a readable, functional manner”, which “allows for greater readability when chaining several functions together”, to quote the proposal.
With the operator “the following invocations are equivalent:”
let result = exclaim(capitalize(doubleSay("hello"))); result //=> "Hello, hello!" let result = "hello" |> doubleSay |> capitalize |> exclaim; result //=> "Hello, hello!"
Seems pretty neat. Really comes in handy when you want to emphasize flow of data through a pipeline of functions in a concise way and you don’t need names or breakpoints for intermediate results. A useful tool for functional programming. No wonder all these languages have it!
So,
Because, while intending otherwise, you’d be making this picture more true:
(credit to the Leftover Salad comic)*
Why? Because operators in JavaScript don’t work like in those neat functional languages. You can’t overload (you might sort of fake it with hacks like this, but please don’t) or define custom operators in JS. They are also not first class citizens. Can’t pass ’em as arguments or return ’em from functions.
So we’re missing some fundamental parts that other languages have which make operators more useful in those languages. But even in those languages operators have some downsides, such as:
too much of them can make code look cryptic,
you need to have a precedence table memorized to understand and use them right,
overloading them and defining custom ones gives some potential for abuse.
These downsides may be important reasons why JavaScript is not big on operators in the first place. Furthermore, even the operators that JavaScript already has come with problems, such as doing some funky coercions of operands.
So stitching a new operator onto the language just because some snippets in other languages look fresh’n’cool, without thinking through if it fits well into this particular language, may not be such a good idea.
If all we care about is the amount of features, why not throw in some more operators, while we’re at it? Could be useful!
Oh, and you can’t polyfill an operator.
We can still have most of stuff that make the pipeline operator so cool, without the downsides, and some additional upsides! Most, so you gonna have to trade a tiny bit off, but I promise it’s nothing significant and absolutely worth it.
What? How? Well, check this baby out:
const pipe = (...args) => args.reduce((prev, curr) => curr(prev))
What’s that? Fire up your console and try it out right away! Just paste this and execute, trust me:
const pipe = (...args) => args.reduce((prev, curr) => curr(prev)) const result = pipe("hello" , $ => $ + ", " + $ , $ => $[0].toUpperCase() + $.slice(1) , $ => $ + '!' ) console.log(result)
Holy moly, this works just like the awesome operator! And you can use it right this second!
If you replace const
with var
and arrow
functions with regular ones you might even be able to make this work in
crummy old browsers too!
So there you go, one line of code, no need to add a special new operator. Thank you for your attention.
But wait!
You had to write the line of code and who needs that! Perhaps there
is a library that implements something like this? Sure there is
are. Of course each has slightly different definition of it. And of
course to import a library you still have to write at least one line of
code. So a built-in operator beats that.
But wait!
How about we build the function into the language? No need to implement your own or to figure out how and which of the seven slightly different versions to import while avoiding bringing in twelve million gigabytes of useless junk along. Being an internal function, it can be optimized to oblivion, too.
So what I’m suggesting is: instead of adding the
|>
operator to JavaScript, add something equivalent to
pipe
above, as Function.pipe
or even in the
global context (e.g. window.pipe
).
A simple function, easy to add, fits well within the language, without the aforementioned downsides. It also has some neat extra goodies.
pipe
better?
It’s a regular function, so:
No need to meddle in the parser, precedence tables and whatnot to add it to the language.
It can be easily polyfilled, as demonstrated!
Arguably it’s easier to learn/understand/use for programmers unfamiliar with the pipeline operator.
It’s first-class! You can pass, return, and compose it neatly with other functions. Whatever you like!
It’s variadic, so you can do useful stuff like:
// pseudo-map of data source url to corresponding pipeline // imagine data fetched from each url has a different format // // the first step in the pipeline ingests that format, // the last spits out some common format const data_sources_to_pipelines = [ [source_1_url, [source_1_function_1, source_1_function_2]], [source_2_url, [source_2_function_1]], [source_3_url, [source_3_function_1, source_3_function_2, source_3_function_3]] ] // the common format could then be processed by a common pipeline, // which runs it through some more steps until it's fully baked const common_pipeline = [common_function_1, common_function_2, common_function_3] // you could then use something like this to obtain // an array of processed data from all the sources: const data_processed = data_sources_to_pipelines.map(([url, pipeline]) => pipe(fetch(url), ...pipeline, ...common_pipeline) )
Try doing things like that with your precious little operator!
pipe
worse?
It’s a regular function, so:
It may not stick out from the surrounding code as much as the operator.
It’s not as clean syntactically as the operator.
It’s variadic, so optimization might be a little more involved than for the fixed-arity operator, as @hajile at HN points out.
First, how to make it stick out more? Here’s a simple way. Compare:
let result = "hello" |> doubleSay |> capitalize |> exclaim
with
let result = pipe("hello" , doubleSay , capitalize , exclaim )
Not that different, are they?
If you use this special leading-comma convention for
pipe
only, it’ll stand out alright! This could even be
enforced by linters, code editors, etc.
If you want something fancier, you could have custom highlighting for
pipe
. You could probably have your IDE
display pipe
as unicode ᐅ
or whatever.
Surely if editors, IDEs, linters, etc. can treat stuff like JSX and whatnot specially, they could also do the same (even easier!) for regular built-in functions.
Next, some arguments in favor of the operator mention words like
conciseness, convenience, cleanliness, ergonomy, and whatnot. Let’s
compare |>
to pipe
with that in mind.
Given the same arguments/operands, in the pipe
case we
have a fixed additional cost at the beginning (have to prepend
pipe(
) and end (have to append )
), but this is
mostly for reading, as autocompletion will do half the typing and
balancing for you here.
On the other hand we have less keys to press per argument
([,]
vs [shift] + [|], [>]
).
So at less than a handful of arguments we’re even, and after that
pipe
gets cheaper both to write and to read, even
disregarding autocompletion.
Should pipe
be placed inside the Function
built-in object, you may also need to put a line like
const { pipe } = Function
atop a file that uses it, for convenience. Your editor might help you
out with this while autocompleting though. Alternatively,
pipe
could be a property of the global scope, like
fetch
or
atob.
So you could just use it right away, like the operator. But then we may
upset this guy:
So maybe take one for the team here?
As for the optimization, I’m sure engine implementers can handle it.
All in all, I think we did well. Didn’t we?
If you people still believe that the operator is really what the language needs right now, consider introducing it along with a more general and powerful extension to JavaScript: make all operators first-class, like functions, so you can pass them around. Also, add the ability to overload and define custom operators. This seems like a better idea than adding a new operator every time enough people think it’d be cool.
It’s a major thing though. Personally, I’m on the fence here, leaning more towards the “against” side. I actually like the fact that JS is fairly restricted and minimal in some aspects.
Either way, if the pressure is already past the point of no return
and the operator must be added, consider bringing
pipe
along.
Or do nothing. Or whatever.
Anyway, I tried.
To use
Douglas
Crockford’s metaphor, I think pipe
would be a better
part than the pipeline operator. It may not look as cool, but it’s
definitely “a backwards-compatible way of streamlining chained
function calls in a readable, functional manner”, which “allows
for greater readability when chaining several functions together”.
Putting in bad parts is real easy, especially with good intentions.
BTW they’re trying to add it to TypeScript too. I’d advise against, on the same grounds.
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.
Anybody up for implementing that?!
If you liked pipe
, try also this other fantastic
flavor:
const compose = (...args) => args.reduceRight((prev, curr) => curr(prev)) // yum! const result = compose(exclaim, capitalize, doubleSay, "hello") const logged = (fn) => (...args) => { console.log('calling', fn) return fn(...args) } const memoized = (fn) => { const cache = new WeakMap() return (arg) => { if (cache.has(arg)) return cache.get(arg) const result = fn(arg) cache.set(arg, result) return result } } const special_square = compose( logged, memoized, (a) => a * a )
So… decorative?
Check out some more mad stuff you could do with
pipe
:
const { pipe: ᐅ } = Function const { PI: π } = Math const double = (n) => n * 2; const increment = (n) => n + 1; const result = ᐅ(π , double , increment , double ) // -> 14.566370614359172
Wow! Pretty cool! Just kidding, don’t do it.
Thanks to ditam for finding the “JavaScript: I’m technically functional” image source.