JavaScript Interview Prep
Functions Deep Dive

Function Composition

The Pipeline

LinkedIn Hook

replaceSpaces(toLowerCase(trim(" Hello World "))).

Read that again. Three function calls. You read it inside-out. Your brain parses trim first, then toLowerCase, then replaceSpaces — but the code is written in the opposite order.

Function composition fixes that. With pipe, you write the steps in the order they run: pipe(trim, toLowerCase, replaceSpaces). Input flows left-to-right through the functions like data through a factory assembly line. Raw input goes in, each station transforms it, a finished value comes out.

compose is the same idea but right-to-left — matching the mathematical f(g(h(x))) notation.

In this lesson you'll implement both pipe and compose in one line each, build a text-cleaning pipeline, a data-transformation pipeline, and learn point-free style — writing functions without ever naming the data they operate on.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #FunctionComposition #Pipe #Compose #FunctionalProgramming #CodingInterview


Function Composition thumbnail


What You'll Learn

  • What function composition is and why pipe(f, g)(x) equals g(f(x))
  • The difference between pipe (left-to-right) and compose (right-to-left)
  • Point-free style and how composition makes it possible

The Assembly Line

Function composition is combining two or more functions to produce a new function. The output of one function becomes the input of the next. If you have functions f and g, composing them gives you f(g(x)).

Think of it like a factory assembly line, but instead of adding parts, each station transforms the product. Raw metal goes in, Station 1 cuts it, Station 2 bends it, Station 3 paints it. Each station is a pure function. The assembly line is composition.

Basic Composition (Manual)

const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const replaceSpaces = str => str.replace(/\s+/g, "-");

// Manual composition — read inside-out
const slug = replaceSpaces(toLowerCase(trim("  Hello World  ")));
console.log(slug); // "hello-world"

// Problem: deeply nested calls are hard to read
// replaceSpaces(toLowerCase(trim(removeSpecialChars(validate(input)))))

Implement compose (Right-to-Left)

function compose(...fns) {
  return function(x) {
    return fns.reduceRight((acc, fn) => fn(acc), x);
  };
}

const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const replaceSpaces = str => str.replace(/\s+/g, "-");

// compose reads right-to-left: replaceSpaces(toLowerCase(trim(x)))
const slugify = compose(replaceSpaces, toLowerCase, trim);

console.log(slugify("  Hello World  "));    // "hello-world"
console.log(slugify("  JavaScript IS Fun ")); // "javascript-is-fun"

Implement pipe (Left-to-Right)

function pipe(...fns) {
  return function(x) {
    return fns.reduce((acc, fn) => fn(acc), x);
  };
}

// pipe reads left-to-right: trim -> toLowerCase -> replaceSpaces
const slugify = pipe(
  str => str.trim(),
  str => str.toLowerCase(),
  str => str.replace(/\s+/g, "-")
);

console.log(slugify("  Hello World  "));     // "hello-world"
console.log(slugify("  JavaScript IS Fun ")); // "javascript-is-fun"

compose vs pipe

const add1 = x => x + 1;
const double = x => x * 2;
const square = x => x * x;

// compose: right-to-left -> square(double(add1(3)))
const composed = compose(square, double, add1);
console.log(composed(3)); // square(double(4)) = square(8) = 64

// pipe: left-to-right -> add1 -> double -> square on 3
const piped = pipe(add1, double, square);
console.log(piped(3)); // add1(3)=4 -> double(4)=8 -> square(8)=64

// Same result, same functions, different reading order
// pipe is generally more readable for most developers

Practical Example: Text Processing Pipeline

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
const trim = str => str.trim();
const removeExtraSpaces = str => str.replace(/\s+/g, " ");
const addPeriod = str => str.endsWith(".") ? str : str + ".";

const cleanSentence = pipe(
  trim,
  removeExtraSpaces,
  capitalize,
  addPeriod
);

console.log(cleanSentence("  hello   world  "));      // "Hello world."
console.log(cleanSentence("  javaScript  is   fun ")); // "JavaScript is fun."
console.log(cleanSentence("already done."));           // "Already done."

Practical Example: Data Transformation Pipeline

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

const filterActive = users => users.filter(u => u.active);
const sortByName = users => [...users].sort((a, b) => a.name.localeCompare(b.name));
const formatNames = users => users.map(u => `${u.name} (${u.role})`);
const joinWithComma = arr => arr.join(", ");

const getActiveUserSummary = pipe(
  filterActive,
  sortByName,
  formatNames,
  joinWithComma
);

const users = [
  { name: "Zara", role: "admin", active: true },
  { name: "Ali", role: "user", active: false },
  { name: "Mina", role: "editor", active: true },
  { name: "Bob", role: "user", active: true }
];

console.log(getActiveUserSummary(users));
// "Bob (user), Mina (editor), Zara (admin)"

Point-Free Style

// Point-free: function is defined without explicitly mentioning its arguments

// NOT point-free (mentions 'x')
const doubleNums = arr => arr.map(x => x * 2);

// Point-free (no mention of the data)
const double = x => x * 2;
const doubleAll = arr => arr.map(double); // still references 'arr'

// Truly point-free with composition
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const map = fn => arr => arr.map(fn);
const filter = fn => arr => arr.filter(fn);

const isEven = x => x % 2 === 0;
const doubleIt = x => x * 2;

// No mention of data at all — completely point-free
const getDoubledEvens = pipe(
  filter(isEven),
  map(doubleIt)
);

console.log(getDoubledEvens([1, 2, 3, 4, 5, 6])); // [4, 8, 12]

Function Composition visual 1


Common Mistakes

  • Mixing up pipe and compose argument order — pipe(f, g, h)(x) runs f first; compose(f, g, h)(x) runs h first. Pick one convention per codebase and stick with it.
  • Composing functions with incompatible signatures — each step must produce a value shaped like the next step's input. A function that returns void or a different type breaks the pipeline.
  • Trying to compose impure functions and expecting predictable results — composition assumes each step is a pure transformation. Mutation inside one step will leak into every downstream step.

Interview Questions

Q: What is function composition?

Function composition is combining two or more functions so the output of one becomes the input of the next. Given functions f and g, composition produces a new function where compose(f, g)(x) equals f(g(x)).

Q: What is the difference between pipe and compose?

compose applies functions right-to-left: compose(f, g, h)(x) = f(g(h(x))). pipe applies functions left-to-right: pipe(h, g, f)(x) = f(g(h(x))). Same result, different reading order. pipe is generally considered more readable.

Q: What is point-free style?

Point-free style is defining functions without explicitly mentioning the data (arguments) they operate on. Instead of x => double(x), you write double directly. It's achieved through composition and curried helper functions.

Q: Implement a pipe function.

const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); — takes an array of functions, returns a new function that passes its argument through each function left-to-right.

Q: Implement a compose function.

const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x); — identical shape to pipe but uses reduceRight so functions apply right-to-left, matching the mathematical f(g(h(x))) order.


Quick Reference — Cheat Sheet

FUNCTION COMPOSITION — QUICK MAP

Definition:
  Combine fns so output of one feeds the next.
  compose(f, g)(x) = f(g(x))
  pipe(f, g)(x)    = g(f(x))

One-line implementations:
  const compose = (...fns) => x =>
    fns.reduceRight((acc, fn) => fn(acc), x);

  const pipe = (...fns) => x =>
    fns.reduce((acc, fn) => fn(acc), x);

Reading order:
  pipe     left -> right   (reads like the steps run)
  compose  right -> left   (matches math f(g(h(x))))

Requirements for clean composition:
  - Each fn is unary (takes one arg)
  - Each fn's output matches the next fn's input shape
  - Functions are pure (no hidden side effects)

Point-free style:
  Build fns from other fns without naming the data.
  pipe(filter(isEven), map(double))     // no 'x', no 'arr'

Previous: Function Currying Next: Debouncing & Throttling


This is Lesson 6.6 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page