The proposal “Pipe operator (|>
) for JavaScript” (by J. S. Choi, James DiGioia, Ron Buckton and Tab Atkins) introduces a new operator. This operator is an idea borrowed from functional programming that makes applying functions more convenient in many cases.
This blog post describes how the pipe operator works and what its use cases are (there are more than you might expect!).
There originally were two competing proposals for a pipe operator, inspired by other programming languages:
F# by Microsoft is a functional programming language whose core is based on OCaml. This pipe operator works together well with curried functions (I’ll explain what that is soon).
Hack by Facebook is – roughly – a statically typed version of PHP. This pipe operator focuses on language features other than curried functions. It includes syntactic support for partial application.
The latter proposal won. However, if you prefer the F# pipe, there is good news: Its benefits may be added to the Hack pipe at a later time (details are explained later).
We’ll start by exploring the Hack pipe. Then we’ll move on to the F# pipe and compare its pros and cons with the pros and cons of the Hack pipe.
This is an example of using the Hack pipe operator |>
:
assert.equal(
'123.45' |> Number(%), 123.45
);
The left-hand side of the pipe operator |>
is an expression that is evaluated and becomes the value of the special variable %
. We can use that variable on the right hand side. The returns the result of evaluating its right-hand side. In other words, the previous example is equivalent to:
assert.equal(
Number('123.45'), 123.45
);
The following examples demonstrate that %
really works like any other variable:
value |> someFunction(1, %, 3) // function calls
value |> %.someMethod() // method call
value |> % + 1 // operator
value |> [%, 'b', 'c'] // Array literal
value |> {someProp: %} // object literal
value |> await % // awaiting a Promise
value |> (yield %) // yielding a generator value
We’ll see more use cases later, but let’s quickly look at a core use case now. Consider these nested function calls:
const y = h(g(f(x)));
This notation usually does not reflect how we think about the computational steps. Intuitively, we’d describe them as:
x
.f()
to it.g()
to the result.h()
to the result.y
.The Hack pipe operator lets us express this intuition better:
const y = x |> f(%) |> g(%) |> h(%);
The F# pipe operator is roughly similar to the Hack pipe operator. However, it doesn’t have the special variable %
. Instead, expects a function at its right-hand side and applies that function to its left-hand side. Therefore, the following two expressions are equivalent:
'123.45' |> Number
Number('123.45')
The following three statements are equivalent:
const y = h(g(f(x))); // no pipe
const y = x |> f(%) |> g(%) |> h(%); // Hack pipe
const y = x |> f |> g |> h; // F# pipe
We can see that Hack pipe is more verbose than F# pipe in this case.
F# pipe works well in functional programming languages with built-in support for currying.
What is currying? Normal (uncurried) functions can have zero or more parameters – for example:
const add2 = (x, y) => x + y;
assert.equal(
add2(3, 5), 8
);
Curried functions have at most one parameter – they are unary. Functions with more parameters are emulated via unary functions that return functions:
const addCurry = x => y => x + y;
assert.equal(
addCurry(3)(7), 10
);
Currying makes it easy to create functions where the initial arguments are partially applied (filled in). For example, these are three ways of defining the same function:
const f1 = addCurry(1);
const f2 = add2.bind(null, 1);
const f3 = x => add2(1, x);
With currying, piping into a function with more than one parameter is concise:
assert.equal(
5 |> addCurry(1), 6
);
Alas, currying has downsides when used in JavaScript:
Currying can only fill in initial parameters. That works well in functional programming languages where the parameter with the data to operate on comes last (e.g. append(elem, list)
). However, JavaScript’s functions are not structured that way and methods are often used, too.
I dislike using the same operator – function invocation – for both function calls and partial application: I prefer seeing immediately if a function is called or partially applied. With currying, I often can’t tell the difference – especially if I don’t know the signature of a function.
Currying doesn’t work with the named parameter pattern: We can’t curry a function that uses this pattern.
Currying doesn’t work with parameter default values: If omitting parameters triggers partial application, we can’t use it to trigger default values.
For more information, see “Currying is not idiomatic in JavaScript”.
%
performs partial application The %
in Hack pipe expressions could be considered an operator for partial application. For example, the following two expressions are equivalent:
5 |> add2(1, %) // Hack pipe
5 |> $ => add2(1, $) // F# pipe
await
, yield
To use the following constructs, we need arrow functions:
value |> $ => $.someMethod() // method call
value |> $ => $ + 1 // operator
value |> $ => [$, 'b', 'c'] // Array literal
value |> $ => {someProp: $} // object literal
For await
and yield
, we’d need special syntax – e.g.:
value |> await // awaiting a Promise
value |> yield // yielding a generator value
With the F# pipe, we can use a unary function to destructure an input value:
const str = obj |> ({first,last}) => first + ' ' + last;
With the Hack pipe we have to either avoid destructuring:
const str = obj |> %.first + ' ' + %.last;
Or we have to use an immediately-invoked arrow function:
const str = obj |> (({first,last}) => first + ' ' + last)(%);
Should do-expressions ever be added to JavaScript, we could destructure via a variable declaration:
const str = obj |> do { const {first,last} = %; first + ' ' + last };
There are three common kinds of use cases for the pipe operator:
We’ll explore these use cases via Hack pipe, but they are also use cases for F# pipe.
All iterators created by the JavaScript standard library have a common prototype. That prototype is not directly accessible, but we can retrieve it like this:
const IteratorPrototype =
Object.getPrototypeOf(
Object.getPrototypeOf(
[][Symbol.iterator]()
)
)
;
With the pipe operator, the code becomes easier to understand:
const IteratorPrototype =
[][Symbol.iterator]()
|> Object.getPrototypeOf(%)
|> Object.getPrototypeOf(%)
;
Mixin classes are a pattern where we use functions as factories for subclasses to emulate multiple inheritance. For example, these are two mixin classes:
const Parsable = (Sup) => class extends Sup {
// ···
};
const Printable = (Sup) => class extends Sup {
// ···
};
Both return a subclass of a given class Sup
. Without the pipe operator, these mixins are used as follows:
class Block extends Printable(Parsable(Object)) {
// ···
}
With the pipe operator, we get:
class Block extends Object |> Parsable(%) |> Printable(%) {
// ···
}
With the pipe operator, we can write functions such as myProcessor
that post-process values in some manner:
const myPostProcessedValue = myValue |> myProcessor(%);
We can see that the Hack pipe is more verbose here than the F# pipe would be. More on that later.
Consider the following function:
function myFunc() {
// ···
return someObject.someMethod();
}
How would we change this code to log the result of someObject.someMethod()
before it is returned?
Without the pipe operator, we’d have to introduce a temporary variable or wrap a function call around the operand of return
.
With the pipe operator, we can do this:
function myFunc() {
// ···
return theResult |> (console.log(%), %); // (A)
}
In line A, we used the comma operator. To evaluate the following expression:
(expr1, expr2)
JavaScript first evaluates expr1
and then expr2
and then returns the latter result.
In the following code, the value that we post-process is a function – we add a property to it:
const testPlus = () => {
assert.equal(3+4, 7);
} |> Object.assign(%, {
name: 'Test the plus operator',
});
The previous code is equivalent to:
const testPlus = () => {
assert.equal(3+4, 7);
}
Object.assign(testPlus, {
name: 'Testing +',
});
We could also have used the pipe operator like this:
const testPlus = () => {
assert.equal(3+4, 7);
}
|> (%.name = 'Test the plus operator', %)
;
Tagged templates are one way of post-processing template literals. The pipe operator can also do that:
const str = String.raw`
Text with
${3} indented
lines
` |> dedent(%) |> prefixLines(%, '> ');
Note that template literals have access to more data than functions we apply via pipe. They are therefore considerably more powerful – for example, String.raw
can only be done via a template literal.
If a pipe-triggered function call is enough, we get the benefit of being able to apply multiple post-processing operations at the same time and can even combine those with a template literal (as is done in the example).
Thanks to the pipe operator, we can chain operations similarly to how we can chain method invocations:
const regexOperators =
['*', '+', '[', ']']
.map(ch => escapeForRegExp(ch))
.join('')
|> '[' + % + ']'
|> new RegExp(%)
;
This code is easy to read and less verbose than introducing intermediate variables.
We can chain methods such as the Array methods .filter()
and .map()
. However:
With the pipe operator, we can chain functions as if they were methods – without the two aforementioned downsides:
import {Iterable} from '@rauschma/iterable/sync';
const {filter, map} = Iterable;
const resultSet = inputSet
|> filter(%, x => x >= 0)
|> map(%, x => x * 2)
|> new Set(%)
;
Strengths of F# pipe:
Strengths of Hack pipe:
await
and yield
(without special syntax).TC39 is currently only persuing the Hack pipe. Concerns against F# pipe include:
await
and yield
workProgress is being made on the Hack pipe, but F# is not being persued anymore (for details, see the Hack operator proposal).
In this section, we examine ways in which the two operators could be improved. However, it’s not always clear if the added complexity would be worth it.
If JavaScript had a partial application operator (as proposed here), then using F# pipes would look almost the same as using Hack pipes:
// Hack pipe
const resultArray = inputArray
|> filter(%, str => str.length >= 0)
|> map(%, str => '['+str+']')
|> console.log(%)
;
// F# pipe with partial application operator
const resultArray = inputArray
|> filter~(?, str => str.length >= 0)
|> map~(?, str => '['+str+']')
|> console.log
;
A few downsides remain, though:
+
.await
and yield
.%
We could make Hack pipe less verbose in the unary case:
%
in the right-hand side of the |>
operator?|>
works like the Hack pipe.|>
works like the F# pipe.Example:
const resultArray = inputArray
|> filter(%, str => str.length >= 0)
|> map(%, str => '['+str+']')
|> console.log // (A)
;
Note that line A is different than the normal Hack pipe.
A proposal for this approach has been discarded, but it could be revived as an add-on for Hack pipes.
We could complement the Hack pipe with a special operator |>>
for unary functions that works like the F# pipe (line A):
const resultArray = inputArray
|> filter(%, str => str.length >= 0)
|> map(%, str => '['+str+']')
|>> console.log // (A)
;
In this section, we look at alternatives to the pipe operator. They are quite elegant but all have the following downsides:
import {Iterable} from '@rauschma/iterable/sync';
const {filter, map} = Iterable;
const resultSet = inputSet
|> filter(%, x => x >= 0)
|> map(%, x => x * 2)
|> new Set(%)
;
Function.pipe()
One alternative to a pipe operator is to use a function – for example, the proposed Function.pipe()
:
const resultSet = Function.pipe(
inputSet,
$ => filter($, x => x >= 0)
$ => map($, x => x * 2)
$ => new Set($)
);
Thanks to arrow functions, this approach is less verbose than we might expect.
Its downsides are:
await
and yield
.We could use intermediate variables:
const filtered = filter(inputSet, x => x >= 0);
const mapped = map(filtered, x => x * 2);
const resultSet = new Set(mapped);
On one hand, that’s more verbose than piping. On the other hand, the variable names describe what is going on – which can be useful if a step is complicated.
The following technique is a variation of the previous technique – it reuses a short variable name such as $
:
let $ = inputSet;
$ = filter($, x => x >= 0);
$ = map($, x => x * 2);
const resultSet = new Set($);
(I’m not sure who deserves credit for this technique; I’ve first seen it used here.)
We save characters due to the shorter variable name and because we don’t need one variable declaration per step.
A downside of this technique is that it can’t be used multiple times within the same scope. And there is no simple way of wrapping a code block around such a code fragment, either.
However, we can fix that via an immediately-invoked Arrow function:
const resultSet = (
($ = inputSet) => {
$ = filter($, x => x >= 0);
$ = map($, x => x * 2);
$ = new Set($);
return $;
}
)();
Props to @emnudge for suggesting using a parameter default value.
Is there an interesting use case for the pipe operator that I missed? Let us know in the comments.