
compose, and especially pipe, are easily among my favorite functions.
This article’s just to have fun and explore different implementations of these two gems. I recommend you understand what they do before reading this; perhaps check out my deep-dive here.
pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);Classic.
Starting with the leftmost function, reduce an array of functions to a single value by calling the next function with the previous one’s output.
double = (x) => x * 2;
add1 = (x) => x + 1;
pipe(
double,
add1
)(100); // 201I discovered this implementation through Eric Elliott, and wrote a deep-dive on it here.
Use reduceRight to implement compose. Now your functions are called from right, to left.
compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);
compose(
double,
add1
)(100);
// 202You could also reverse fns and keep using reduce (less performant).
compose = (...fns) => (x) => fns.reverse().reduce((v, f) => f(v), x);
compose(
double,
add1
)(100); // 202reverse mutates the array, though, so you might copy it first (even less performant).
compose = (...fns) => (x) => [...fns].reverse().reduce((v, f) => f(v), x);
compose(
double,
add1
)(100); // 202Use reduceRight to go back to pipe.
pipe = (...fns) => (x) => [...fns].reverse().reduceRight((v, f) => f(v), x);
pipe(
double,
add1
)(100); // 201But They’re All Unary
All the above snippets, by the way, are unary. Each function may only accept a single argument.
If your pipeline’s first function must be nAry (accepting n arguments), try this implementation:
multiply = (x, y) => x * y;
pipe = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));
pipe(
multiply,
add1
)(10, 10); // 101
// Takes multiple args nowThis snippet’s from 30secondsofcode.org. Your first (leftmost) function may accept n arguments–all others must be unary.
Again, reduceRight gives us compose. Now your rightmost function may accept n arguments. Let’s move multiply to the end of the chain.
compose = (...fns) => fns.reduceRight((f, g) => (...args) => g(f(...args)));
compose(
add1,
multiply
)(10, 10); // 101
// Takes multiple args now
// Put multiply firstLike before, you could reverse the fns array and keep using reduce:
compose = (...fns) =>
[...fns].reverse().reduce((f, g) => (...args) => g(f(...args)));
compose(
add1,
multiply
)(10, 10); // 101If you want to keep reduce without the slight performance hit, just switch g and f:
compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
compose(
add1,
multiply
)(10, 10); // 101And use reduceRight to switch back to pipe.
pipe = (...fns) => fns.reduceRight((f, g) => (...args) => f(g(...args)));
pipe(
multiply,
add1
)(10, 10); // 101
// put multiply first nowConclusion
Phew! That’s a lot of ways to pipe and compose!
It just proves that, no matter what, you must loop over an array of functions, calling the next one with the previous one’s result.
Doesn’t matter if you use reduce, reduceRight, switch the invocation order, or whatever else.
If you want
pipe(), go left-to-right. Want compose()? Go right-to-left.
Plain and simple. Until next time!