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 asyncPipe(...fns). It returns a function that passes its input through each function in turn, left to right, and returns a promise of the final result. Each function may return a plain value or a promise; wait for it to fulfill, then call the next function with the value.
The first function gets every argument the piped function was called with; the others get one value each. With no functions, the result is the first argument. The piped function always returns a promise, even when every step is synchronous. If a step throws or rejects, that promise rejects with the error, and the steps after it don't run.
const getUserName = asyncPipe(
(id) => fetchJson("/api/users/" + id), // returns a promise
(user) => user.name, // returns a plain value
(name) => name.toUpperCase(),
);
await getUserName(42); // "ADA"
const run = asyncPipe(
(x) => x + 1,
(x) => x * 2,
(x) => x - 3,
);
await expect(run(5)).resolves.toBe(9);
const clock = useFakeTimers();
const slowIncrement = (x) => new Promise((resolve) => setTimeout(resolve, 100, x + 1));
const run = asyncPipe(slowIncrement, (x) => x * 10);
let result;
run(1).then((value) => {
result = value;
});
await clock.tickAsync(100);
expect(result).toBe(20);
const promise = asyncPipe((x) => x + 1)(1);
expect(promise).toBeInstanceOf(Promise);
await expect(promise).resolves.toBe(2);
const run = asyncPipe(
(a, b) => a + b,
(sum) => sum * 10,
);
await expect(run(2, 3)).resolves.toBe(50);
await expect(asyncPipe()("same")).resolves.toBe("same");
const error = new Error("step 2 failed");
const later = spy();
const run = asyncPipe(
(x) => x,
async () => {
throw error;
},
later,
);
await expect(run(1)).rejects.toBe(error);
expect(later).not.toHaveBeenCalled();
const error = new Error("bad input");
const run = asyncPipe(() => {
throw error;
});
let promise;
expect(() => {
promise = run(1);
}).not.toThrow();
await expect(promise).rejects.toBe(error);
const run = asyncPipe(
(x) => x + 1,
(x) => x * 2,
);
await expect(run(1)).resolves.toBe(4);
await expect(run(2)).resolves.toBe(6);
await expect(run(3)).resolves.toBe(8);
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 Async 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.