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.
Write pipe(...fns). It returns one function that runs fns from left to right, feeding each result into the next: pipe(f, g, h)(x) is h(g(f(x))).
The first function gets all the arguments and the caller's this; every later function gets the previous result as its only argument.
With no functions, pipe() returns a function that returns its first argument. If any argument isn't a function, pipe throws a TypeError right away, before anything is called. The piped function can be called any number of times.
const slugify = pipe(
(s) => s.trim(),
(s) => s.toLowerCase(),
(s) => s.replaceAll(" ", "-"),
);
slugify(" Hello World "); // "hello-world"
const doubleSum = pipe((a, b) => a + b, (n) => n * 2);
doubleSum(10, 15); // 50
expect(pipe((x) => x + 1, (x) => x * 2)(3)).toBe(8);
expect(pipe((s) => s + "a", (s) => s + "b", (s) => s + "c")("")).toBe("abc");
expect(pipe((a, b) => a + b, (x) => x * 10)(2, 3)).toBe(50);
const last = spy((x) => x);
pipe((a, b) => a * b, last)(4, 5);
expect(last.calls[0].args).toEqual([20]);
const obj = {
n: 4,
next: pipe(
function () {
return this.n;
},
(x) => x + 1,
),
};
expect(obj.next()).toBe(5);
const value = { id: 1 };
expect(pipe()(value)).toBe(value);
expect(pipe()(5, 6)).toBe(5);
const addAB = pipe((s) => s + "a", (s) => s + "b");
expect(addAB("")).toBe("ab");
expect(addAB("")).toBe("ab");
expect(addAB("x")).toBe("xab");
expect(() => pipe((x) => x, null)).toThrow(TypeError);
expect(() => pipe({})).toThrow(TypeError);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Pipe. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.