JavaScript Shinies: Pipeline Operator

In the final instalment of this ongoing series we look at a JavaScript feature that is further off than usual. Pipelines are a “functional” approach that takes a bit of getting used to.

Pipelines are one of the least “here” bits of functionality we’ve looked at so far. New JavaScript features are added as part of an RFC process by a group called the TC39. The proposals to them go through stages. They start as 0, which is like when you’re drunk (or stoned, who am I to judge) and you say to your friends “Hey, you know what would be cool?”

Then they get a bit more focused and go to stage 1 as a formal proposal. Like you woke up the next day and had to do a business plan.

Stage 2 is when things start to get firmed up, syntax clarified and determined, edge cases explored, etc. It’s often possible to get experimental code running at this point, with things like Babel potentially supporting it.

At Stage 3 there’s a pretty good chance browsers are implementing it. Except Microsoft. Obviously. This is a comprehensive spec at this point. You can’t have any emoji in your business plan.

The final is stage 4, which is shipping. It’s part of the standard, it’s expected to be supported by current browsers. Except Microsoft.

Most of the stuff we’ve looked at previously is stage 4. Decorators were a stage 2 proposal that I think made it to stage 3 literally while I was writing it. But the pipeline operator is stage 1. It’s early days yet. Nevertheless, JavaScript’s headlong rush towards functional programming, combined with the fact that using this will make you look like a cutting edge guru, make this well worth a look.

What Problem Is This Solving?

A lot of JavaScript is object oriented, and that’s fine. But a lot also isn’t. Simple functions are often the clearest way to express what you want to do, especially when transforming numbers or other data.

Examples are usually more than a bit abstract, so lets go with something more concrete.

Let’s say we have some simple functions.

const dashify = string => string.replace(/ /g, '-');
const lowercase = string => string.toLowerCase();
const trim = string => string.trim();
const removePunctuation = string => string.replace(/[^\w\s]|_/g, "");

You can now use simple functions like this grouped together to create a “slugified” string. Let’s make it ugly first.

let myString = "Javascript Shinies: Pipeline Operator";
myString = trim(myString);
myString = removePunctuation(myString);
myString = lowercase(myString);
const slug = dashify(myString);
console.log(slug); // javascript-shinies-pipeline-operator

Most people reading, though, will see that the myString variable is doing very little here. It’s actually not really needed. Rather than constantly reassigning the string we could directly use the functions inside each other.

const slug = trim(removePunctuation(lowercase(dashify("Javascript Shinies: Pipeline Operator")))));

One liner! That’s always better, right? Right? Of course!

That said, it’s shit to read. And we’ve made it so that our actual function calls are literally reversed from what they were before, because the functions execute from the inside (right) to the outside (left). The order matters a lot here. If you do the dashify step first, the later removePunctuation function will strip the dashes out.

We also have the issue that we have to literally count brackets. Did you notice that there was one too many closing brackets? Because I didn’t.

The pipeline operator, which is written as |>, cleans this up by introducing a special syntax that passes the result of one function as the first argument of the next function. Rewriting our previous examples to use it ends up with more like the first example than the second, only quite a bit shorter. Note again that the brackets aren’t needed because the first argument is always the result of the previous function.

const slug = "Javascript Shinies: Pipeline Operator" |> trim |> removePunctuation |> lowercase |> dashify;

This preserves both our order and our readability. It’s also relatively common to put these sorts of pipelines on multiple lines, and probably preferred.

const slug = "Javascript Shinies: Pipeline Operator" 
|> trim
|> removePunctuation
|> lowercase
|> dashify
|> console.log

A couple of things deserve attention. First of all, the item passed through as the first argument in all these cases is a string. There’s no strict reason for this. The addition of a split function could convert it to an array, later join puts back together. Whatever the function returns is the argument passed to the next function.

As an addendum to this, though, I personally think a pipeline that routinely changes its type and action is a bad idea. If your water pipes switched to sewerage and then to natural gas and back to water, you’d probably be a little concerned. But yes, you absolutely can convert from type to type, strings, arrays, objects, etc.

The case for that illustrates the next point. While the above example removes brackets and arguments entirely that’s only because it removes the first argument and these functions had an arity of one. (Arity is the number of arguments a function takes.)

Let’s make some new functions, these ones actually need arguments beyond the one passed in. Both splitting and joining are good examples of this, so we’ll replace our dashify function with a split and join based example, and assume the above functions also still exist.

const split = (string, separator) => string.split(separator);
const join = (array, glue) => array.join(glue);
const lowercase = string => string.toLowerCase();
const trim = string => string.trim();
const removePunctuation = (string) => string.replace(/[^\w\s]|_/g, '');
const slug = "Javascript Shinies: Pipeline Operator" 
|> trim
|> removePunctuation
|> lowercase
|> split(' ')
|> join('-')
|> console.log

A Quick Personal Note About Functional Programming in Javascript

I have to say as an aside that I’m actually not a very big fan of FP as used in JavaScript, and am lukewarm on FP as a cult… I mean… software paradigm in general.

As an approach it rarely bothers to meet any burden of proof, and proponents make a lot of unsupported claims and strawman arguments. Recently I read an article that bravely pointed out that bad JavaScript with for loops is worse than functional programming. Only the Functional version was actually just OOP, and also it was pretty bad too.

There are Functional Programming languages that are exceptional, such as Elixir. But IMO JavaScript isn’t one of them. In as far as FP principles and approaches can be squished into JS they have to be forced pretty hard, and are reliant on the underlying OO model anyway. See the functions we made above? Those are just “functionalising” the already existing OO functionality, and in fact we had to use the OO approach internally to make them work. Do we gain much over this?

const slug = "Javascript Shinies: Pipeline Operator"
.split(' ')

I’m not convinced we do. To clarify, I’m not arguing against its use in something like Elixir, whose built-in functions are all set out to do exactly this.

slug = "Javascript Shinies: Pipeline Operator"
|> String.trim
|> String.downcase
|> String.split(" ")
|> Enum.join("-")

Nevertheless, this article is not about functional programming in general, it’s about the pipeline operator.

Better Cases for Pipelines

Better examples for use in pipelines exist in other libraries. Many people would be familiar with moment.js for example, as a date library. This is almost aggressively OOP: moment().add(1, 'days').calendar()

But a much smaller and simpler replacement library I recommend is date-fns which uses utility functions on default JS date objects. All of its functions are pure functions, so they have no effect other than what is passed into them. Typically they take a date object, and return a date object, which is perfect for what we want to do.

Let’s say I want to get the date a given number of days ago and also last year, formatted how we want it. Big-endian. The correct date format.

let currentTime = new Date();
let dateDaysAgo = subDays(currentTime, numberOfDays);
let oneYearAgo = subYears(dateDaysAgo, 1);
let formattedDate = format(oneYearAgo, 'YYYY-MM-DD');

You can see from this that the first argument is always the result of the previous function, so we know this will be the same.

let formattedDate = format(subYears(subDays(new Date(), numberOfDays), 1), 'YYYY-MM-DD');

Getting a bit hard to read though, right? The functions read backwards, the outermost being the last one executed even though it’s the first read, and the argument order is incredibly unclear. Pipelines cut the cruft without the confusion.

let formattedDate = new Date 
|> subDays(numberOfDays)
|> subYears(1)
|> format('YYYY-MM-DD');

Another place pipelines make a bit of sense are in some parts of the react/redux system, where (from my own production system) code like this is common for actions.


If we use pipelines we may well get a bit more clarity here. |> removeNotification |> dispatch

There’s something kind of elegant about this. It’s all subjective, of course, but I like the way this dispatch action happens at the end. Do this, do that, then dispatch.

There’s nothing to pipelines that really unlocks powerful functionality like proxies, or can greatly simplify code, like rest/spread syntax. But as a bit of icing on the cake it can be a nice feature.

Enabling this Feature

As with the previous, nothing is supported here out of the box. This feature is thoroughly in the box. And the box is in a cupboard. This is also too new a feature to see it in the wild in frameworks and common libraries. But that doesn’t stop us playing.

Also as before the requirement is an npm install and a babel setup. I’m going to assume (probably wrongly) that you have the babel command-line installed or some sort of build tool that calls it.

npm install @babel/plugin-proposal-pipeline-operator --save-dev

Then your .babelrc file needs to have the following code.

"plugins": [
{ "proposal": "minimal" }]

The proposal property on the object there refers to three competing syntaxes for pipelines. They all use the same operator, |> but some of them have variants, in particular how they handle async functions is quite notable. None of our examples have had asynchronous functions, and if they had they wouldn’t have worked. Unless of course the pipeline was intended to take promises, which seems unlikely.

For now a niche toy, but potentially a useful one.