Write pipe(...fns) and compose(...fns) that combine functions into one.
Before you start
What's function composition?
Function composition means building one function out of several small ones, where the result of each becomes the input of the next. Written as nested calls, f(g(x)) runs g on x, then f on what g returned. Each small function does one job, can be tested on its own, and can be reused in other combinations. It works like an assembly line: every station does one thing to the item and passes it on.
Here's a title turned into a URL slug in three steps, written as nested calls:
const trim = (text) => text.trim();
const lower = (text) => text.toLowerCase();
const hyphenate = (text) => text.replaceAll(" ", "-");
const title = " Hello World ";
trim(title); // "Hello World"
lower(trim(title)); // "hello world"
hyphenate(lower(trim(title))); // "hello-world"
Nested calls read inside out: trim runs first but is written last, and with five or six steps the brackets get hard to follow. So libraries offer helpers that take the functions as a list and return the combined function. compose takes them in the same order as the nested calls and runs them right to left, the way composition is written in maths. pipe takes them in the order they run, left to right, the way you read:
| Written as | The functions are listed | They run | Result |
|---|---|---|---|
hyphenate(lower(trim(title))) | Outermost first | trim, lower, hyphenate | "hello-world" |
compose(hyphenate, lower, trim)(title) | Right to left: the last runs first | trim, lower, hyphenate | "hello-world" |
pipe(trim, lower, hyphenate)(title) | Left to right: the first runs first | trim, lower, hyphenate | "hello-world" |
The first step gets whatever the combined function is called with. A function returns one value, so every step after it gets a single argument: the previous step's result. Steps can also be async. When a step in an async pipe returns a promise, the pipe waits for it and hands the value it resolves to on to the next step, so the whole pipeline returns a promise. If a step fails, the steps after it don't run.
You meet composition in Lodash's _.flow (a pipe) and _.flowRight (a compose), in Redux's compose, used to combine store enhancers, and in RxJS, where observable.pipe(map(...), filter(...)) runs operators left to right.
Both return a single function that runs the functions you pass, one after another. pipe runs them left to right: pipe(f, g, h)(x) is h(g(f(x))). compose runs them right to left: compose(f, g, h)(x) is f(g(h(x))).
The first function to run gets all the arguments, and the caller's this. Each of the others gets one value: the result of the function that ran before it.
With no functions, return the identity function, which returns its argument unchanged. If any argument isn't a function, throw a TypeError right away, when pipe or compose is called.
function pipe(...fns) {
// your code
}
function compose(...fns) {
// your code
}
// const slugify = pipe((s) => s.trim(), (s) => s.toLowerCase(), (s) => s.replaceAll(" ", "-"));
// slugify(" Hello World "); // "hello-world"
//
// pipe((x) => x + 1, (x) => x * 2)(3); // 8: (3 + 1) * 2
// compose((x) => x + 1, (x) => x * 2)(3); // 7: 3 * 2 + 1
// pipe((a, b) => a + b, (x) => x * 10)(2, 3); // 50: only the first function gets both arguments
// pipe()(5); // 5
Solve it in the editor, with tests
The answer
Write it yourself first. Then compare it with a reference solution and the checks it passes.
Checking your sign-in…