Compose and Pipe Helpers
Composing small functions into a single transformation is the bread and butter of functional pipelines. This snippet contrasts the right-to-left `compose` (math notation) with the left-to-right `pipe` (data-flow notation), then shows an async-aware `pipeAsync` for chained `await`-able steps. Use them to flatten nested calls into a readable left-to-right (or right-to-left) sequence.
954 views
16
function pipe(...fns) {
return (input) => fns.reduce((acc, fn) => fn(acc), input);
}
const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const slug = (s) => s.replace(/\s+/g, '-');
const toSlug = pipe(trim, lower, slug);
console.log(toSlug(' Hello World ')); // hello-world
console.log(toSlug('Functional JS')); // functional-jsPipe reads top-to-bottom in source code: the first function receives the input, and each subsequent function transforms the previous result. Array.prototype.reduce over the function list with the input as the seed is the entire implementation. This is the order most developers find intuitive (data flows left-to-right, like a Unix shell pipeline) and it matches the proposed pipe operator (|>). Use pipe whenever the human reading the call site cares more about data flow than about mathematical composition.
function compose(...fns) {
return (input) => fns.reduceRight((acc, fn) => fn(acc), input);
}
const double = (n) => n * 2;
const addOne = (n) => n + 1;
const square = (n) => n * n;
// compose runs right-to-left: square first, then addOne, then double
const f = compose(double, addOne, square);
console.log(f(3)); // square(3)=9 -> addOne=10 -> double=20Compose runs the rightmost function first, mirroring math notation f(g(h(x))). Switching reduce to reduceRight is the only change versus pipe. This convention is the default in Haskell, Ramda, and Redux's middleware composition, so it keeps muscle memory consistent for developers coming from those ecosystems. Whether you prefer compose or pipe is mostly stylistic, but in mixed teams pick one and stick with it: stacking both in the same module is the surest way to confuse the next reader.
function pipeAsync(...fns) {
return async (input) => {
let acc = input;
for (const fn of fns) {
acc = await fn(acc);
}
return acc;
};
}
const fetchUser = async (id) => ({ id, name: `User${id}` });
const loadProfile = async (user) => ({ ...user, bio: `bio of ${user.name}` });
const tag = async (profile) => ({ ...profile, role: 'admin' });
const hydrate = pipeAsync(fetchUser, loadProfile, tag);
hydrate(42).then((p) => console.log(p));
// { id: 42, name: 'User42', bio: 'bio of User42', role: 'admin' }Real-world pipelines mix sync and async steps (parse JSON, then fetch, then transform). pipeAsync awaits each step before passing the result to the next, so any function in the chain can be either synchronous or async and the chain just works. The explicit for..of (instead of reduce) is necessary because reducing across Promises requires either then-chaining manually or a top-level await, and the loop form is the cleanest expression of "resolve, then continue". Pair this with retry / timeout decorators to build full data pipelines in user code.
