Currying
Currying transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument, where each call returns a new function until all arguments have been supplied.
// Normal function
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
// Curried version
function curriedAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
With arrow functions, this reads more compactly:
const curriedAdd = (a) => (b) => (c) => a + b + c;
console.log(curriedAdd(1)(2)(3)); // 6
Why it’s useful — partial application
Currying lets you “pre-fill” some arguments and get back a specialized function, reusable with different remaining arguments.
const multiply = (a) => (b) => a * b;
const double = multiply(2); // pre-fill `a` with 2
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// A practical example: building a reusable logger
const log = (level) => (message) => console.log(`[${level}] ${message}`);
const logError = log("ERROR");
const logInfo = log("INFO");
logError("Something broke"); // "[ERROR] Something broke"
logInfo("Server started"); // "[INFO] Server started"
A generic curry helper
A general-purpose curry function that works for any arity, calling the original function once enough arguments have accumulated.
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...nextArgs) {
return curried.apply(this, [...args, ...nextArgs]);
};
};
}
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6, mixed grouping works too
console.log(curriedSum(1, 2, 3)); // 6, calling with all args at once also works
Function composition
Composition combines several single-argument functions into one, where each function’s output feeds into the next — a natural pairing with curried/partially-applied functions from above.
const compose =
(...fns) =>
(initialValue) =>
fns.reduceRight((acc, fn) => fn(acc), initialValue); // right to left
const pipe =
(...fns) =>
(initialValue) =>
fns.reduce((acc, fn) => fn(acc), initialValue); // left to right
const double = (n) => n * 2;
const increment = (n) => n + 1;
const square = (n) => n * n;
const composed = compose(square, increment, double); // square(increment(double(x)))
console.log(composed(3)); // double(3)=6, increment(6)=7, square(7)=49
const piped = pipe(double, increment, square); // square(increment(double(x)))
console.log(piped(3)); // same order of operations, read left-to-right instead
// A practical pipeline: each step takes one argument and returns one value,
// exactly what curried functions naturally produce
const clampToZero = (n) => Math.max(0, n);
const applyDiscount = (rate) => (price) => price * (1 - rate); // curried
const roundToCents = (n) => Math.round(n * 100) / 100;
const finalPrice = pipe(clampToZero, applyDiscount(0.2), roundToCents);
console.log(finalPrice(99.99)); // 79.99, discount and rounding applied in sequence
compose reads “mathematically” (rightmost function runs first, like f(g(h(x)))), while pipe reads like a left-to-right pipeline — most real-world code favors pipe for readability.