Skip to content

Aquent | DEV6

Future JS: The Pipeline Operator

Written by: James McGeachie

JavaScript is often considered a “multi-paradigm” language. Its current features lend itself to writing both object-oriented and functional code. Lately an interest in writing functional JavaScript has spiked, and with that, a demand for more language features that empower a functional style. One such feature under consideration is the Pipeline Operator, a proposal that allows us to compose functions together more cleanly, by piping the result of one function directly to the next.

Function Composition

When following the functional programming paradigm, functions are first class citizens in your codebase.  That means you’ll spend a lot of time composing small, single-purpose functions together. Let’s have a look at how you may do that with current JS features.

The example we’re going to use throughout this post is a simple phone number formatter. It takes a phone number with any formatting, cleans it up and then outputs a version formatted to our desired pattern.

function formatPhoneNumber(number) {
  const stripped = number.replace(/\D/g,'');
  const digitGroups = stripped.match(/^(\d{3})(\d{3})(\d{4})$/);
  return `${digitGroups[1]}-${digitGroups[2]}-${digitGroups[3]}`;
}

console.log(formatPhoneNumber('555.012 (3456)'));
// 555-012-3456

There’s our function! It does 3 things:

  1. Strips non-numeric characters
  2. Finds digit groups
  3. Hyphenates the digit groups to match 012-345-6789 format

That works… but wait, we said it does 3 things. Functions that do several things are harder to test, and also are a sign that you may have reusable portions you can split out. Let’s split it up.

function stripNonNumeric(text) {
  return text.replace(/\D/g,'');
}

function findDigitGroups(number) {
  return number.match(/^(\d{3})(\d{3})(\d{4})$/);
}

function formatDigitGroups(groups) {
  return `${groups[1]}-${groups[2]}-${groups[3]}`;
}

const result = formatDigitGroups(findDigitGroups(stripNonNumeric('555.012 (3456)')));
console.log(result);
// 555-012-3456

Now we have 3, single line functions, each that has a separate task, and is potentially reusable. Great! One problem though…this line here:

const result = formatDigitGroups(findDigitGroups(stripNonNumeric('555.012 (3456)')));

Oaft. That’s a long line, kind of nasty and difficult to read. Let’s try indenting it. 

const result = formatDigitGroups(
  findDigitGroups(
    stripNonNumeric('555.012 (3456)')
  )
);

Not much better. Arguably worse. It’s still difficult to follow what’s going on. There are a few problems that make this confusing:

  1. There are several levels of nesting, which harms readability
  2. To understand the flow, we have to go to the innermost-function call first, and work our way back

With current JS, probably what you’d want to do here is split it up into multiple variable assignments, as follows:

const digits = stripNonNumeric('555.012 (3456)');
const groups = findDigitGroups(digits);
const result = formatDigitGroups(groups);

This does read better, although since the variables are one-use, we’re making a redundant assignment for the sake primarily of readability. This isn’t a huge problem to be fair, but it is a bit of extra mental overhead coming up with variable names and could have some minor performance overhead as you’re assigning more values to memory.

Wouldn’t it be great if we could get the best of both worlds – readable function composition, without redundant steps? Turns out we can, with the Pipeline Operator.

Pipelines

One of the proposals currently going through the TC39 process (the steps by which any feature gets added to the JavaScript language spec) is for a Pipeline Operator. This is inspired by similar features in other languages, like the forward pipeline operator in Microsoft’s F#. You can check out the proposal on the github repo here:

https://github.com/tc39/proposal-pipeline-operator

Now as you’ll see if you have a look at that link, this is still very much an ongoing discussion. Multiple proposals are under consideration. However, they all stem from a base proposal. Let’s have a look at our above example with the pipeline operator

const result = '555.012 (3456)'
  |> stripNonNumeric
  |> findDigitGroups
  |> formatDigitGroups;

console.log(result);
// 555-012-3456

Let’s break down what’s happening here.

  • We specify our input string
  • We pipe it to the first function, stripNonNumeric
  • We pipe the result of that function to the next, findDigitGroups
  • Finally, we pipe the result of findDigitGroups to formatDigitGroups

The beauty of this pipeline comes from a simple assumption – that we’re continuing to operate on the same value that’s being passed through these functions from left to right. This means we don’t need to explicitly pass it each time. The next function is given the return value from the previous one.

I find the end result much more readable than any of the previous examples, because:

  • We’ve eliminated the nesting
  • The order of operations now reads forwards rather than backwards
  • There’s no unnecessary variable assignment

Much nicer, right?

When can we use this?

The short answer is, you can use it today… if you use the babel compiler as part of your build process. There’s a plugin available here:

https://babeljs.io/docs/en/babel-plugin-proposal-pipeline-operator

However, you may want to take caution using a feature still under discussion. The syntax could change radically, making your codebase out of date with the official language spec. This is particularly risky with the pipeline operator, as there are several proposals under consideration

As is often the case, this proposal wasn’t as simple as it initially seems. It turns out there are many problems that needed to be solved before this operator was ready for public use. These problems include:

  • Syntax for piping async functions
  • Syntax for passing multiple arguments to a piped function
  • Compatibility with other spec proposals (for non-pipeline features)

The different proposals have different opinions on how to deal with these and until compromise can be reached from all parties, we won’t see this feature in the language proper.

Further Reading

If you’d wish to read more about pipelines and the ongoing discussion, the wiki on the github repo has a great breakdown of the proposals that have been considered to date:

https://github.com/tc39/proposal-pipeline-operator/wiki

If you’re interested in the inspiration behind the feature, the F# function docs here are a good read (See section ‘Function Composition and Pipelining’):

https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/index

Please also stay tuned for further updates on this topic in future DEV6 blogs. Thanks for reading!