Currying with Placeholders
advancedThe harder follow-up: support _ placeholders so arguments can arrive in any order.
Implement a curry function in JavaScript with arity collection, the fn.length gotchas with defaults and rest params, and the infinite-sum variant interviewers add.
Implement
curry(fn)so that a 3-argument function can be called asf(1)(2)(3),f(1, 2)(3),f(1)(2, 3), orf(1, 2, 3)— all returning the same result.
Currying is the canonical closure exercise: the state being closed over is "the arguments collected so far," and every partial call creates a new closure holding a longer prefix. If you can narrate where those arguments live and why sibling partials don't interfere, the implementation writes itself.
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args); // enough arguments — actually run
}
return function (...nextArgs) { // not yet — return a collector
return curried.apply(this, [...args, ...nextArgs]);
};
};
}The whole mechanism is one comparison: args.length >= fn.length — "do I have as many arguments as the function declares?" Each partial call recurses into curried with the accumulated prefix, so every stage gets the same decision logic.
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
curriedAdd(1)(2)(3);
// call 1: args=[1] 1 < 3 → return collector closing over [1]
// call 2: args=[1,2] 2 < 3 → return collector closing over [1,2]
// call 3: args=[1,2,3] 3 >= 3 → add(1,2,3) → 6
const add1 = curriedAdd(1);
add1(2)(10); // 13 — and add1 is REUSABLE:
add1(5)(10); // 16 — each call from add1 builds its own fresh chainThat reusability is the point worth saying out loud: partials are values. curriedAdd(1) isn't a step in a protocol — it's a new function you can store, pass, and reuse, and none of its descendants share mutable state.
fn.length gotchas (where interviewers poke)The elegance rests on fn.length — and fn.length is more fragile than people think:
((a, b) => {}).length; // 2
((a, b = 1) => {}).length; // 1 — stops counting at the first default!
((a, ...rest) => {}).length; // 1 — rest params don't count
((a, b) => {}).bind(null, 1).length; // 1 — bind subtracts bound argsSo curry((a, b = 2) => a + b) fires after one argument, and a rest-param function can never collect more than its declared count. The honest fix is an explicit arity: curry(fn, arity = fn.length). Volunteering this limitation — and the parameter that patches it — is the difference between having memorized curry and understanding it.
Constantly conflated, and a guaranteed follow-up:
| Currying | Partial application | |
|---|---|---|
| Transforms | one n-ary fn → chain of calls collecting args | one n-ary fn → one fn with some args pre-filled |
| Calls to get a result | as many as it takes to reach arity | exactly one more |
f(1) returns | another collector (until arity) | — you chose the split up front: partial(f, 1) |
One-liner: currying is automatic, repeatable partial application. In production JavaScript you'll use ad-hoc partials (arrow wrappers, bind) far more often than true currying — configuration-then-data pipelines (const format = formatWith(locale); items.map(format)) are the realistic use, and saying so beats overselling FP purity.
Make
sum(1)(2)(3)...()work — terminate on the empty call.
No arity to compare against, so termination needs an explicit signal:
function sum(...args) {
const total = args.reduce((a, b) => a + b, 0);
const next = (...more) =>
more.length === 0 ? total : sum(total, ...more);
return next;
}
sum(1)(2, 3)(4)(); // 10The interviewer's real question is "how do you end an unbounded chain?" — empty call, .value() method, or coercion via Symbol.toPrimitive/valueOf (so sum(1)(2) + 0 works). Name at least two; implement one.
curriedAdd(1, 2, 3, 4): pass through (>=, not ===) and let fn ignore extras, matching plain JS call semantics; a strict-arity variant is a stated policy, not a fix.this forwarding — the apply(this, ...) chain matters if anyone curries a method; also why the collectors are functions, not arrows.curry(() => 42)() must work: 0 >= 0 on the first call. Falls out naturally; know that it does.[...args, ...nextArgs], never push).args.push(...)) — sibling partials now corrupt each other; the spread-copy is load-bearing.fn.length quirks and being unable to explain why curry broke on a defaulted parameter.this forwarding is in scope.fn.length; explicit arity parameter — same discussion as the gotchas above.Parameters, tuple slicing) and get hairy fast; typing fixed arities (curry2, curry3) is the pragmatic move. Knowing where the type system strains is itself signal.connect(mapState)(Component), middleware signatures like store => next => action — reading those is reading curry.The harder follow-up: support _ placeholders so arguments can arrive in any order.
Compose functions left-to-right into a pipeline — the one-liner with a lot of interview depth.
Pre-fill leading arguments of a function — partial application, and how it differs from currying.
Guarantee a function runs exactly once and returns its cached result forever after.