Lesson 5 of 8
Function Composition
Combining simple functions to build complex operations with compose and pipe
What is Function Composition?
Function composition is the process of combining two or more functions to produce a new function. It's like building with LEGO blocks - simple pieces snap together to create complex structures. In math, composition is written as (f ∘ g)(x) = f(g(x)).
// Manual composition
const add1 = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
// Compose manually: square(double(add1(5)))
const result = square(double(add1(5)));
// add1(5) = 6, double(6) = 12, square(12) = 144
// Create a composed function
const transform = x => square(double(add1(x)));
transform(5); // 144
The compose Function
compose combines functions from right to left (like mathematical composition).
// Simple compose for two functions
const compose2 = (f, g) => x => f(g(x));
const add1ThenDouble = compose2(double, add1);
add1ThenDouble(5); // double(add1(5)) = double(6) = 12
// Compose for any number of functions
const compose = (...fns) => x =>
fns.reduceRight((acc, fn) => fn(acc), x);
// Read right-to-left: add1 → double → square
const transform = compose(square, double, add1);
transform(5); // 144
// More practical example
const sanitize = str => str.trim();
const lowercase = str => str.toLowerCase();
const slugify = str => str.replace(/\s+/g, '-');
const createSlug = compose(slugify, lowercase, sanitize);
createSlug(' Hello World '); // 'hello-world'
The pipe Function
pipe is like compose but goes left to right, which many find more intuitive.
// Pipe - left to right (data flows like water through pipes)
const pipe = (...fns) => x =>
fns.reduce((acc, fn) => fn(acc), x);
// Read left-to-right: add1 → double → square
const transform = pipe(add1, double, square);
transform(5); // 144
// More readable for data transformations
const processUser = pipe(
validateInput,
normalizeEmail,
hashPassword,
saveToDatabase
);
// Practical example
const processText = pipe(
str => str.trim(),
str => str.toLowerCase(),
str => str.split(' '),
words => words.filter(w => w.length > 3),
words => words.join(', ')
);
processText(' The QUICK Brown Fox ');
// 'quick, brown'
Point-Free Style
Point-free (or tacit) programming is writing functions without mentioning their arguments.
// With points (explicit argument)
const double = x => x * 2;
const numbers = [1, 2, 3];
const doubled = numbers.map(x => double(x));
// Point-free (no explicit argument)
const doubled = numbers.map(double);
// More examples
// With points
const getNames = users => users.map(user => user.name);
// Point-free with composition
const prop = key => obj => obj[key];
const getNames = users => users.map(prop('name'));
// Building point-free functions
const isEven = n => n % 2 === 0;
const not = fn => (...args) => !fn(...args);
const isOdd = not(isEven);
// Filter examples - point-free
const evens = numbers.filter(isEven);
const odds = numbers.filter(isOdd);
// Caution: Point-free isn't always clearer
// Sometimes explicit is better for readability
Real-World Composition Patterns
// Data validation pipeline
const validators = {
required: value => value !== '' ? null : 'Required',
email: value => /@/.test(value) ? null : 'Invalid email',
minLength: min => value =>
value.length >= min ? null : `Min ${min} chars`,
};
const validate = (...rules) => value =>
rules.map(rule => rule(value)).filter(Boolean);
const validateEmail = validate(
validators.required,
validators.email,
validators.minLength(5)
);
validateEmail(''); // ['Required', 'Invalid email', 'Min 5 chars']
validateEmail('ab'); // ['Invalid email', 'Min 5 chars']
validateEmail('a@b.com'); // []
// API response transformation
const processApiResponse = pipe(
response => response.data,
data => data.users,
users => users.filter(u => u.active),
users => users.map(u => ({ id: u.id, name: u.name })),
users => users.sort((a, b) => a.name.localeCompare(b.name))
);
// Middleware pattern (like Express)
const applyMiddleware = (...middlewares) => handler =>
middlewares.reduceRight(
(next, middleware) => middleware(next),
handler
);
const withLogging = next => request => {
console.log('Request:', request);
return next(request);
};
const withAuth = next => request => {
if (!request.token) throw new Error('Unauthorized');
return next(request);
};
const handleRequest = applyMiddleware(
withLogging,
withAuth
)(request => ({ success: true }));
Async Composition
// Compose async functions (Promises)
const composeAsync = (...fns) => x =>
fns.reduceRight(
(promise, fn) => promise.then(fn),
Promise.resolve(x)
);
const pipeAsync = (...fns) => x =>
fns.reduce(
(promise, fn) => promise.then(fn),
Promise.resolve(x)
);
// Example async pipeline
const fetchUser = async id => {
const res = await fetch(`/api/users/${id}`);
return res.json();
};
const getPostsByUser = async user => {
const res = await fetch(`/api/posts?userId=${user.id}`);
return res.json();
};
const sortByDate = posts =>
[...posts].sort((a, b) => new Date(b.date) - new Date(a.date));
const getUserPosts = pipeAsync(
fetchUser,
getPostsByUser,
sortByDate
);
// Usage
const posts = await getUserPosts(123);
✅ Composition Best Practices
- • Keep functions small and focused (single responsibility)
- • Functions should take one argument when possible
- • Use
pipefor left-to-right readability - • Name composed functions descriptively
- • Ensure types match between composed functions
- • Consider point-free only when it improves clarity