Infinite Currying in JavaScript
Return a function until a sentinel (empty call or valueOf) asks for the sum. Closures, toString tricks, and why this is a closure test, not a math test.
- javascript
- closures
- currying
- interview
The prompt is sum(1)(2)(3)() and they want 6. Someone reaches for a fixed-arity curry(fn) keyed off fn.length, writes sum as (a, b, c) => a + b + c, and then cannot explain a fourth (4). Wrong problem.
Infinite currying in this interview is not “turn f(a, b, c) into f(a)(b)(c) until arity is met.” That is the currying and partial application page. Here the arity is unknown. You keep returning a function that closes over a running total until a sentinel asks for the number: an empty call, or a coercion via valueOf / toString / Symbol.toPrimitive.
If you treat it as algebra, you will overfit to addition. If you treat it as a closure that accumulates arguments, the same skeleton does product, concat, or a logger.
- Return a function from every value-taking call. The chain can be arbitrarily long.
- A terminator (empty invocation, or a primitive coercion) is what yields the total.
- The running total lives in a closure. That is the test.
fn.lengtharity curry is a different exercise. Link it, don’t reuse it.valueOf/toStringmakesum(1)(2) + 3work. They also surpriseconsole.log.
Keep returning a function until someone asks for the number
Two common sentinels, sometimes both on the same object:
| Sentinel | Call site | Mechanism |
|---|---|---|
| Empty call | sum(1)(2)(3)() |
arguments.length === 0 (or b === undefined if you accept that footgun) |
| Coercion | sum(1)(2) + 0, `${sum(1)(2)}` |
valueOf, toString, Symbol.toPrimitive |
Empty-call terminator:
function sum(a) {
return function next(b) {
if (arguments.length === 0) return a;
return sum(a + b);
};
}
sum(1)(2)(3)(); // 6
sum(1)(2)(3)(4)(); // 10
Each return value is a new function whose closure holds the updated total. sum(a + b) is a cheap factory; you are not mutating a single running function, you are handing back the next stage.
arguments.length === 0 is the right empty check. if (b === undefined) treats sum(1)(undefined)() as “stop now” and drops the explicit undefined argument. If the interviewer cares, say you terminate only on a true empty call.
Coercion terminator
Some prompts omit the final (). They want sum(1)(2)(3) + 4 === 10. Ordinary functions coerce to the source text, which is useless here. You attach valueOf (and usually toString) on the returned function.
function sum(a = 0) {
function next(b) {
if (arguments.length === 0) return a;
return sum(a + b);
}
next.valueOf = function valueOf() {
return a;
};
next.toString = function toString() {
return String(a);
};
next[Symbol.toPrimitive] = function toPrimitive(hint) {
return hint === 'string' ? String(a) : a;
};
return next;
}
sum(1)(2)(3) + 4; // 10
Number(sum(1)(2)); // 3
String(sum(1)(2)); // "3"
sum(1)(2)(); // 3
+ asks for a primitive with hint "default", which for objects other than Date ends up in valueOf first. Template strings hint "string" and prefer toString. Implementing Symbol.toPrimitive makes the hint explicit so you are not debugging ToPrimitive under a whiteboard clock. The type coercion guide is the long form.
console.log(sum(1)(2)) will print a function, or a number, depending on whether the host calls toString. Don’t promise a specific DevTools rendering.
This is a closure test
The interesting state is a, not the +. A product chain is the same object with a * b. A string builder uses a + String(b). The shape is:
- Close over an accumulator.
- On a value, return a new function with the updated accumulator.
- On a sentinel, return the accumulator.
function accumulate(combine, initial) {
function stage(acc) {
function next(value) {
if (arguments.length === 0) return acc;
return stage(combine(acc, value));
}
next.valueOf = () => acc;
return next;
}
return stage(initial);
}
const prod = accumulate((a, b) => a * b, 1);
prod(2)(3)(4)(); // 24
If you cannot explain why next still sees acc after stage returned, you are not done. Read closures: it is a live binding, not a snapshot, which only matters if you mutate acc instead of passing a new one. Prefer the new-stage style above so mutation never enters the talk.
Not the arity curry
Currying and partial application stops when args.length >= fn.length. sum in this problem has no useful .length if you define it as a rest function, and the whole point is that the caller may pass five numbers or two. Don’t check fn.length. Don’t use bind. Don’t build curry(add) unless they asked for that page’s problem.
Partial application of a known binary add(a, b) is also a different sentence: add.bind(null, 1) returns a one-arg function, not an infinite chain.
Footguns of infinite currying
sum()with no seed. Decide:0, or throw. The coercion form above usesa = 0sosum()()is0.- Non-numbers.
'1' + 2concatenates. Coerce withNumberif the prompt is numeric, and say whatNaNdoes. - Forgetting to return
next. Returningatoo early kills the chain. - Single shared
nextplus mutableacc.const x = sum(1); const y = x(2); x(3)();should not polluteyif each call returns a new function. Mutation of one accumulator makes those aliases fight. toStringfor display vs value. A function’s defaulttoStringis source. Overriding it makes the object lie in string contexts. Fine for the trick; mention the lie.
Interview out loud
“I return a new function every time I see a number, closing over the running total. An empty call is the sentinel that returns the total; if they want
sum(1)(2) + 3I also implementvalueOfandSymbol.toPrimitive. I terminate onarguments.length === 0, not onundefined, so an explicit undefined still counts as a value. This is not arity-based curry: I do not look atfn.length, and I would send that variant to the currying page. The same closure shape works for product or concat. I return a new stage each time so two chains from the same prefix do not share mutable state.”
Further reading
Related on this site
- Currying and partial application
- Closures
- Type coercion rules
- Compose and pipe
- call, apply, and bind
- Implement function once
Related guides
- Implement Function OnceWrap a function so it runs at most once and then returns the first result. this, arguments, thrown errors, and async once that still shares the first promise.
- Analytics SDK Event QueueQueue analytics events, send them in series, retry the ones that fail, and flush on page hide without dropping the last click.
- Arrays That Emit Change EventsWrap push, pop, and splice so subscribers hear adds and removes. Proxy vs subclass, and why mutating via index assignment still slips through.
- Array.filter PolyfillWrite filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- Array.find PolyfillWrite Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.