Chapter 5.6☕ 20 min read

Higher-Order Functions, Currying & Composition

Function lo, function do — Currying se todo, Composition se jodo.

01Higher-Order Functions — Functions as Data

A Higher-Order Function (HOF) is a function that takes a function as an argument, returns a function, or does both. JavaScript is built on HOFs — map, filter, reduce, forEach, sort, addEventListener, even setTimeout are all Higher-Order Functions.

A callback is simply a function passed to another function to be called later. When you pass a function to map, that function is a callback.

// Built-in HOFs
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => n * 2);       // map is a HOF
const evens = numbers.filter(n => n % 2 === 0); // filter is a HOF

// Custom HOF — repeat an action n times
function repeat(n, action) {
  for (let i = 0; i < n; i++) {
    action(i);
  }
}

repeat(3, (i) => console.log('Iteration:', i));
// Iteration: 0
// Iteration: 1
// Iteration: 2

// Custom HOF — unless condition met
function unless(condition, fn) {
  if (!condition) fn();
}

unless(false, () => console.log('Condition was false!')); // Runs
unless(true, () => console.log('This will not run'));      // Skipped

HOFs enable abstraction: they separate what to do from how many times to do it. The repeat function handles the loop — you just provide the action.

V8 Optimization: When V8 encounters a Higher-Order Function like map or filter, TurboFan attempts to inline the callback function directly into the loop body. This eliminates the overhead of the function call entirely, making array methods nearly as fast as raw for-loops in optimized code.
02Currying: One Argument at a Time

Currying transforms a function that takes multiple arguments f(a, b, c) into a chain of unary functions f(a)(b)(c). Each call returns a new function waiting for the next argument. It does NOT call the function — it transforms how the function receives its arguments.

The simplest curried function uses closures:

// Manual currying
const add = (a) => {
  return (b) => {
    return a + b;
  };
};
// Or shorter: const add = a => b => a + b;

console.log(add(2)(3)); // 5

// Creating specialized functions (reuse)
const add5 = add(5);
const add10 = add(10);

console.log(add5(3));  // 8
console.log(add10(3)); // 13

// Generic curry helper (simplified)
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...moreArgs) {
        return curried.apply(this, args.concat(moreArgs));
      };
    }
  };
}

// Using the curry helper
function multiply(a, b, c) { return a * b * c; }
const curriedMultiply = curry(multiply);

console.log(curriedMultiply(2)(3)(4)); // 24
console.log(curriedMultiply(2, 3)(4)); // 24 — flexible!

Currying enables specialization: create a base function once, then specialize it for specific use cases. add5 and add10 are specialized versions of add.

03Partial Application vs Currying

Partial Application fixes SOME arguments of a function and returns a new function for the rest. Currying fixes ONE argument at a time, returning a chain of unary functions. Currying is a specific form of partial application.

// Original function
function log(level, timestamp, message) {
  console.log('[' + level + '] ' + timestamp + ': ' + message);
}

// Partial Application — fixing level
const warnLog = (timestamp, message) => log('WARN', timestamp, message);
const errorLog = (timestamp, message) => log('ERROR', timestamp, message);

warnLog(Date.now(), 'Disk space low'); // [WARN] 169...: Disk space low

// Currying — one argument at a time
const curriedLog = level => timestamp => message =>
  console.log('[' + level + '] ' + timestamp + ': ' + message);

const curriedWarn = curriedLog('WARN');
const curriedWarnNow = curriedWarn(Date.now());

curriedWarnNow('Cache miss'); // [WARN] 169...: Cache miss

// Partial via bind
const boundWarn = log.bind(null, 'WARN');
boundWarn(Date.now(), 'Network slow'); // [WARN] 169...: Network slow

bind is a form of partial application — it fixes this and optionally some arguments, returning a new function for the rest. Arrow closures are the simplest way to partially apply.

📋 Real-world note: In real-world JS, strict currying (only one argument at a time) is less common than partial application. Libraries like Lodash provide _.partial and _.curry that allow flexible argument application. The functional programming library Ramda uses auto-currying by default on all its functions.
04Function Composition: Pipe & Compose

Composition combines two or more functions to produce a new function. Compose executes right-to-left: compose(f, g, h)(x) = f(g(h(x))). Pipe executes left-to-right, which is more readable for data flows: pipe(f, g, h)(x) = h(g(f(x))).

Point-free style defines functions without mentioning the data argument — making code declarative and focused on transformations.

// Compose: Right-to-left
const compose = (f, g) => (x) => f(g(x));

// Pipe: Left-to-right
const pipe = (f, g) => (x) => g(f(x));

// Real composition with multiple functions
const composeAll = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const pipeAll = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

// Example: Data transformation pipeline
const trim = str => str.trim();
const toLower = str => str.toLowerCase();
const addPrefix = str => 'hyd-' + str;

const formatInput = pipeAll(trim, toLower, addPrefix);

console.log(formatInput('  HELLO WORLD  ')); // 'hyd-hello world'

// Point-free style
const getName = user => user.name;
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
const getCapitalizedName = pipeAll(getName, capitalize);

console.log(getCapitalizedName({ name: 'sai' })); // 'Sai'

Data flows through the pipeline like an assembly line — each function transforms the data and passes it to the next.

05Practical Patterns & Avoiding Over-engineering

The middleware pattern uses composition — functions wrap other functions, passing data along a chain. Express.js and Redux middleware work this way.

// Middleware pattern via Composition
const logger = next => data => {
  console.log('Input:', data);
  const result = next(data);
  console.log('Output:', result);
  return result;
};

const stringify = next => data => next(JSON.stringify(data));
const addMeta = next => data => next({ timestamp: Date.now(), payload: data });

// Compose middlewares
const processPipeline = pipeAll(
  logger,      // Step 1: Log raw
  addMeta,     // Step 2: Add metadata
  stringify,   // Step 3: Stringify
  logger       // Step 4: Log final
);

processPipeline({ user: 'Sai' });

// Config factory (curried)
const createFetcher = baseUrl => headers => async (endpoint) => {
  const res = await fetch(baseUrl + endpoint, { headers });
  return res.json();
};

// Specialize step by step
const apiFetcher = createFetcher('https://api.dev')({ 'Auth': 'token123' });
// Later...
// const data = await apiFetcher('/users');
Don't over-engineer: Currying and composition are powerful, but don't curry every function "just in case". If a function is always called with all its arguments, keep it simple. Use these patterns where they genuinely simplify data flows and enable reuse. If a composition chain is hard to read, a simple function with intermediate variables is better. Deep composition adds function call overhead — V8 optimizes well, but don't add 50 layers.

Lo kar liya — Key Points:

  • ✅ Higher-Order Functions take or return functions — map, filter, reduce, and custom HOFs are the backbone of JS
  • ✅ Currying transforms f(a,b,c) into f(a)(b)(c) — a chain of unary functions for specialization and reuse
  • ✅ Partial Application fixes SOME arguments (e.g., via bind or arrow closures), currying fixes ONE at a time
  • ✅ Compose chains functions right-to-left; Pipe chains left-to-right — both create data transformation pipelines
  • ✅ Point-free style defines functions without mentioning arguments, making code declarative
  • ✅ Use composition and currying for clarity, not complexity — over-engineering makes code harder to read
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase