Why NOT to add the pipeline operator to JavaScript (or TypeScript, etc.)? And what to maybe add instead.

Darius J Chuck

2018-01-25

This post was originally published here.

TL;DR

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.

Intro

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,

Why not add it to JavaScript as well?

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:

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.

What then?

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.

How is pipe better?

It’s a regular function, so:

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!

How is pipe worse?

It’s a regular function, so:

It’s variadic, so optimization might be a little more involved than for the fixed-arity operator, as @hajile at HN points out.

What can we do about these terrible drawbacks?

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?

NO, YOU DIDN’T CONVINCE ME! I MUST HAVE THE OPERATOR!!!11!

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.

Outro

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.

Appendix

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

Thanks to ditam for finding the “JavaScript: I’m technically functional” image source.

Discussion

On reddit.