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 pipe for left-to-right readability
  • • Name composed functions descriptively
  • • Ensure types match between composed functions
  • • Consider point-free only when it improves clarity