Function Composition

Med

Function composition is the art of combining simple functions to build complex ones. Master currying, partial application, pipe/compose, and middleware patterns to write declarative, reusable code.

Interactive Visualization

CodeSetup
1const add10 = x => x + 10;
2const mult2 = x => x * 2;
3const sub5 = x => x - 5;
4
5const pipe = (...fns) =>
6 x => fns.reduce((v, f) => f(v), x);
7
8const transform = pipe(add10, mult2, sub5);
9transform(5); // ((5+10)*2)-5 = 25
Function Pipeline
add10
(x)
x + 10
mult2
(x)
x * 2
sub5
(x)
x - 5
Output
Three simple functions defined: add10, mult2, sub5
1 / 6
Key Insight: pipe() flows data left-to-right like reading: add10 → mult2 → sub5. Each function receives the previous result.

Key Points

  • Currying transforms f(a, b, c) into f(a)(b)(c) - one arg at a time
  • Partial application fixes some arguments, returns function for the rest
  • pipe() flows left-to-right: pipe(f, g, h)(x) = h(g(f(x)))
  • compose() flows right-to-left: compose(f, g, h)(x) = f(g(h(x)))
  • Point-free style eliminates explicit parameter references
  • Middleware chains handlers: request → handler1 → handler2 → response

Code Examples

Currying

// Manual currying
const add = a => b => c => a + b + c;
add(1)(2)(3);  // 6

// Generic curry function
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...more) => curried(...args, ...more);
  };
}

const curriedAdd = curry((a, b, c) => a + b + c);
curriedAdd(1)(2)(3);  // 6
curriedAdd(1, 2)(3);  // 6
curriedAdd(1)(2, 3);  // 6

Currying enables partial application of any argument

Partial Application

// _.partial fixes arguments from the left
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}

const greet = (greeting, name) => `${greeting}, ${name}!`;
const sayHello = partial(greet, 'Hello');

sayHello('Alice');  // "Hello, Alice!"
sayHello('Bob');    // "Hello, Bob!"

// Real-world: pre-configure API calls
const fetchJSON = partial(fetch, { headers: { 'Content-Type': 'application/json' } });

Partial fixes some args, unlike curry which transforms the signature

Pipe and Compose

// pipe: left-to-right (reading order)
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

// compose: right-to-left (math notation)
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

// Example functions
const add10 = x => x + 10;
const multiply2 = x => x * 2;
const subtract5 = x => x - 5;

// pipe: 5 → add10 → multiply2 → subtract5
pipe(add10, multiply2, subtract5)(5);     // ((5+10)*2)-5 = 25

// compose: subtract5 ← multiply2 ← add10 ← 5
compose(subtract5, multiply2, add10)(5);  // ((5+10)*2)-5 = 25

Pipe reads naturally; compose matches mathematical notation f∘g

Point-Free Style

// With explicit parameters
const getNames = users => users.map(user => user.name);

// Point-free (no explicit parameters)
const prop = key => obj => obj[key];
const map = fn => arr => arr.map(fn);

const getName = prop('name');
const getNames = map(getName);

// Usage
const users = [{ name: 'Alice' }, { name: 'Bob' }];
getNames(users);  // ['Alice', 'Bob']

// Point-free with pipe
const getActiveNames = pipe(
  filter(prop('active')),
  map(prop('name'))
);

Point-free eliminates intermediate variables for cleaner pipelines

_.once() - Execute Once

function once(fn) {
  let called = false;
  let result;

  return function(...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

// Usage: expensive initialization
const initializeApp = once(() => {
  console.log('Initializing...');
  return { config: loadConfig() };
});

initializeApp();  // "Initializing..." + returns config
initializeApp();  // returns cached config (no log)
initializeApp();  // returns cached config (no log)

Once ensures a function runs exactly once, caching the result

Middleware Pattern

// Express-style middleware
function createApp() {
  const middlewares = [];

  return {
    use(fn) {
      middlewares.push(fn);
    },
    async handle(req) {
      let idx = 0;
      const next = async () => {
        if (idx < middlewares.length) {
          await middlewares[idx++](req, next);
        }
      };
      await next();
      return req;
    }
  };
}

const app = createApp();
app.use(async (req, next) => { req.start = Date.now(); await next(); });
app.use(async (req, next) => { req.user = await getUser(req); await next(); });
app.use(async (req, next) => { req.result = process(req); });

await app.handle({ path: '/api' });

Middleware chains functions with next() control flow

Common Mistakes

  • Confusing curry (transforms signature) with partial (fixes args)
  • Forgetting that pipe and compose return functions, not values
  • Over-using point-free style when it hurts readability
  • Not handling async functions in compose/pipe

Interview Tips

  • Implement curry() that handles any arity
  • Implement pipe() and compose() - know the difference
  • Explain when to use currying vs partial application
  • Write a middleware system from scratch

Practice Problems

Apply this concept by solving these 6 problems

Related Concepts