Compose and Pipe
Right-to-left compose vs left-to-right pipe — building unary pipelines, debugging intermediate values, and when a plain function is enough.
- javascript
- compose
- pipe
- functional
compose and pipe chain unary functions so data flows through small steps. Libraries disagree on direction. Interviews care that you pick a direction and stick to it — and that you know compose is usually right-to-left (math style).
Definitions
// compose: f∘g∘h (x) = f(g(h(x))) — right to left
const compose =
(...fns) =>
(x) =>
fns.reduceRight((v, f) => f(v), x);
// pipe: left to right — often easier to read top-to-bottom
const pipe =
(...fns) =>
(x) =>
fns.reduce((v, f) => f(v), x);
const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const exclaim = (s) => `${s}!`;
compose(exclaim, lower, trim)(' Hi '); // "hi!"
pipe(trim, lower, exclaim)(' Hi '); // "hi!"
Same functions, opposite evaluation order. Prefer pipe in application code if your team reads top-down.
Why bother
// nested
exclaim(lower(trim(input)));
// pipeline
pipe(trim, lower, exclaim)(input);
Pipelines keep intermediate transforms named and reorderable. They shine when steps are pure and unary.
Multi-arg at the edges
Only the first function in a pipe can reasonably take multiple args if you adapt:
const pipeArgs =
(f, ...fns) =>
(...args) =>
fns.reduce((v, fn) => fn(v), f(...args));
const area = (w, h) => w * h;
const double = (n) => n * 2;
pipeArgs(area, double)(3, 4); // 24
Or curry the first step. Inside the pipeline, keep functions unary.
Async pipe
const pipeAsync =
(...fns) =>
(x) =>
fns.reduce(async (v, f) => f(await v), x);
const fetchUser = async (id) =>
(await fetch(`/api/users/${id}`)).json();
const pickName = (u) => u.name;
await pipeAsync(fetchUser, pickName)(7);
Errors reject the returned promise — wrap with try/catch at the call site.
Debugging
const tap =
(label) =>
(v) => {
console.log(label, v);
return v;
};
pipe(trim, tap('after trim'), lower, exclaim)(' OK ');
tap is a pure-looking spy that returns its input.
When not to use them
// clearer as a normal function
function normalizeEmail(raw) {
const trimmed = raw.trim().toLowerCase();
if (!trimmed.includes('@')) throw new Error('invalid');
return trimmed;
}
Heavy branching, early returns, and multi-value intermediates fight unary pipelines. Don’t invent compose for two calls.
Point-free style (use lightly)
const scores = users.map(pipe(getScore, clamp0to100, Math.round));
Readable when each name is obvious. Opaque when someone has to jump five files to see the order.
Interview answer
“compose applies right-to-left, pipe left-to-right. Both reduce a list of unary functions. I use pipe for readable data transforms, keep steps pure, and fall back to a named function when control flow gets non-linear. Async variants await each step.”
Related
Typed mental model and arity
Compose/pipe assume each step returns what the next accepts. That’s the real design work — not the 4-line reduce helper.
// explicit types in comments / TS make pipelines maintainable
// string → string → number → string
const summarize = pipe(trim, lower, (s) => s.length, String);
// branching: leave the pipeline
function normalize(input) {
const base = pipe(trim, lower)(input);
if (!base) return null;
return exclaim(base);
}
Library variants (lodash/fp, Ramda) auto-curry and reverse argument order for data-last style. If your codebase isn’t already FP-heavy, a local 5-line pipe beats importing a 50-function toolkit for two transforms.
Further reading
Related guides
- Functional Array PatternsPractical functional array techniques — flatMap, partitioning, indexing, zip, and when chaining hurts performance.
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.