Write a `curry(fn)` helper in JavaScript so that both `curry(fn)(a)(b)(c)` and `curry(fn)(a, b)(c)` produce the same result. How does it decide when to finally call `fn`?
answer
- count arguments, not calls
- compare collected count to arity
- spread old and new into one list
- declared arity comes from fn.length
- call through once enough arrived
basics
~20 sA curry helper returns a collector function that accumulates arguments across calls and invokes the original only once the collected count reaches the function's declared arity, fn.length; until then it returns a new collector holding the arguments gathered so far.
solid answer
~40 sThe helper returns a `curried` function that takes rest arguments. If `args.length >= fn.length` it calls `fn(...args)` and returns the result; otherwise it returns a new function that, when called, restarts `curried` with the old arguments plus the new ones. That single arity check is what makes both call styles work: each call contributes however many arguments it wants, and completion is driven by the count, not by the number of calls. `fn.length` — the number of parameters declared before the first default or rest parameter — is the arity source, which is why the helper only works for functions with plain fixed parameters. A common refinement is `curry(fn, arity = fn.length)` so callers can state the arity explicitly when `fn.length` lies.
code
javascript · 15 linesfunction curry(fn, arity = fn.length) {
return function curried(...args) {
if (args.length >= arity) return fn(...args);
return (...rest) => curried(...args, ...rest);
};
}
const volume = curry((l, w, h) => l * w * h);
console.log(volume(2)(3)(4)); // 24
console.log(volume(2, 3)(4)); // 24
console.log(volume(2)(3, 4)); // 24
const base = volume(2, 3);
console.log(base(4), base(10)); // 24 60 — partials are reusablego deeper
Recall that the helper gathers arguments with a rest parameter and only calls the original function once it has enough of them; be able to read the six-line implementation and say what each line does.
Write the helper from scratch and explain the arity test, why the accumulated arguments are spread into a fresh array per partial, and where fn.length comes from.
Show what breaks in real code: extra arguments from callback APIs completing the call early, declared arity lying because of defaults or rest, and the zero-argument case looping forever.
Weigh whether a shared curry helper belongs in a codebase at all — it is small, but it moves arity errors from call time to some later coercion, and that debugging cost is paid by everyone who reads the stack trace.
## The requirement A "loose" or auto-`curry` helper must satisfy all of these for a three-parameter `fn`: ```js curried(1)(2)(3); curried(1, 2)(3); curried(1)(2, 3); curried(1, 2, 3); ``` So the helper cannot count *calls*; it must count *arguments*. ## The implementation ```js function curry(fn, arity = fn.length) { return function curried(...args) { if (args.length >= arity) return fn(...args); return (...rest) => curried(...args, ...rest); }; } const volume = curry((l, w, h) => l * w * h); volume(2)(3)(4); // 24 volume(2, 3)(4); // 24 volume(2)(3, 4); // 24 ``` Three pieces do all the work: 1. **`...args` rest parameter** — collects however many arguments this call supplied into an array. 2. **The arity test** — `args.length >= arity` asks "do I have enough yet?". Using `>=` rather than `===` means extra arguments are simply forwarded instead of being silently dropped into another round. 3. **The recursive return** — the returned arrow keeps the already-collected `args` alive and re-enters `curried` with the concatenation `(...args, ...rest)`. Each partial call therefore produces an independent accumulation, so `const v2 = volume(2)` can be reused any number of times without the earlier arguments leaking between uses. Because each partial creates a fresh array from spreading, branching is safe: ```js const half = volume(2, 3); half(4); // 24 half(10); // 60 — the earlier call did not consume anything ``` ## Where the arity comes from `fn.length` is the count of parameters declared **before** the first default value or rest parameter. It is exactly what you want for a plain `(l, w, h)` function and exactly wrong for `(a, b = 1)` (`length` is `1`) or `(...xs)` (`length` is `0`). That is why the helper above exposes an optional `arity` parameter: `curry(sum, 3)` lets the caller state the truth when the declaration cannot. ## The zero-argument trap What should `volume(2)()` do? With the code above, `args.length` on the inner re-entry is still `1`, which is below the arity, so you get yet another function back — an infinite chain of no-ops if a caller keeps invoking with nothing. Some helpers treat a zero-argument call as "force the call now", others throw. Whichever you choose, say out loud that you considered it; interviewers frequently probe exactly there. ## Interaction with callback APIs A curried function passed straight into a callback-taking API receives whatever arguments that API supplies, and the `>=` test may consider itself satisfied immediately: ```js const add = curry((a, b) => a + b); [1, 2, 3].map(add); // each call gets (value, index, array) ``` Here `add` is invoked with three arguments on the very first call, so `args.length` is already `3`, the arity test passes, and `fn(value, index, array)` runs with the index as the second operand — you get `[1, 3, 5]`, not an array of functions. Wrap explicitly (`(x) => add(x)(10)`) when handing curried helpers to APIs you do not control. ## Variations an interviewer may push toward - **Named recursion.** The inner function is a *named function expression* (`function curried(...)`) so it can call itself without depending on the outer binding, which keeps the helper working even if the caller reassigns the variable it was stored in. - **Placeholders.** Supporting "skip this slot" requires a sentinel value and a merge step that fills holes left to right; it is a real feature of some utility libraries but well beyond what a whiteboard answer needs. - **Preserving `name`/`length`.** The returned function reports `length` `0` because it declares only a rest parameter; if that matters, define it explicitly with `Object.defineProperty`. ## What good looks like in the interview Write the six lines, then narrate the three decisions: arity source, `>=` versus `===`, and what a zero-argument call does. Adding the `arity` override parameter unprompted signals that you know `fn.length` is unreliable for functions with defaults or rest parameters.
- Why use `args.length >= arity` instead of `args.length === arity`?Because a caller may supply more arguments than declared — directly, or via a callback API that passes extras. With `===`, that call would fall through to the "return another function" branch and never complete, leaving the caller with a function where they expected a value. `>=` forwards the surplus to `fn`, which simply ignores what it does not declare.
- What should `curried()` — a call with no arguments — do?With the plain implementation it adds nothing to the accumulator, fails the arity test, and hands back yet another collector, so a loop of empty calls never terminates. Pick a rule deliberately: either treat a zero-argument call as a no-op that returns the same collector, or throw a `TypeError` so the mistake surfaces immediately instead of turning into a mysterious function-valued result.
- Why is the inner function written as a named function expression rather than an anonymous one?The recursive step calls `curried` by that internal name, which is in scope inside the function expression itself. That keeps the helper self-contained: it does not depend on the variable the caller happened to assign it to, so reassigning or renaming that variable cannot break the recursion.
saying these in an interview costs you the question
- Counts the number of calls instead of the number of collected arguments
- Mutates a single shared args array, so partials contaminate each other
- Uses === for the arity test, so extra arguments never complete the call
- Assumes fn.length is reliable for functions with defaults or rest parameters
- Thinks the helper must know the arity by hand-coding nested arrows