Kata: curry
Never keep the arguments in one place: every call gets a new array, and every partial owns its own.
Predict first
Before you read: predict
ExamHall's question editor builds its length checks from one curried validator factory, used as rule(max)(field)(value). The team's kata answer:
The editor runs const short = rule(10), then short('name')('Ada'), then short('title')('Intro').
What happens?
export function curry(fn) { const args = []; return function curried(...more) { args.push(...more); return args.length >= fn.length ? fn(...args) : curried; };} export const rule = curry((max, field, value) => `${field}: ${value.length <= max ? 'ok' : 'too long'}`);The 30-second version
A kata curry reads the arity from fn.length, collects arguments across calls, and calls fn once at least that many have arrived. Each call builds a new array, [...args, ...more], and returns a new function over it, so every partial can be reused and never changes another.
Never keep the arguments in one place: every call gets a new array, and every partial owns its own.
How it works
Step 1 of 10
f(1)(2)(3), f(1, 2)(3) and f(1, 2, 3) give one result; partials are reusable; extra arguments reach fn.Analogy: A saddler's made-to-measure order card (leatherwork)
A saddler cuts a saddle once its order card gives the tree width, the seat size and the leather.
| At the saddler's bench | In the kata |
|---|---|
| the card goes to the cutting table once its boxes are filled | fn runs once the arity is reached (steps 2, 5) |
| a rider gives one size today and two next week | f(1)(2, 3) (step 7) |
| a riding school's card, tree width already filled, photocopied for each rider | a partial reused; each call copies (step 4) |
| one card on the counter that every rider writes on | the shared array (step 3) |
| the box count printed on top stops at the first box marked "house default" | fn.length (step 2) |
| "count the default box too" written in pencil | an explicit arity (step 8) |
In real terms: curry is a function that keeps returning new functions, each closing over a new array, until enough arguments have arrived to call fn.
Breaks down when: a saddler sees two riders' sizes crowding one card; a shared array fails silently, or later, in someone else's call.
Contrasting case: a card with a "notes" box after the leather: the saddler still starts at three boxes and keeps the notes, as fn gets extra arguments (step 6).
Examples, run for real
4.1 ExamHall EXH-3520: one shared array behind every length check
The question editor's first draft of curry kept one array per curry() call (EXH-3520). It passed its author's single check and then mixed up questions. MR !3820 replaced it with the reference and added the test list to the repository. The list also runs on two common mutants: M1 keeps one array per curry() call, and M2 passes fn only fn.length arguments.
4.1 reference.mjs 11 of 11 pass (9 core, 2 ladder)
4.1 M1, one array per curry() call 5 of 11 pass · red: C2 "f(1, 2)(3) and f(1)(2, 3) return fn's result", C5 "a partial is reusable: add1(2)(3), then add1(10)(20)", C6 "two partials made from one partial stay independent", C7 "two chains from the curried function itself stay independent", L1 "an explicit arity waits for a parameter after a default", L2 "fn gets the caller's this, at once and through a partial"
4.1 M2, fn given only arity arguments 10 of 11 pass · red: C8 "arguments past fn.length reach fn"
4.1 EXH-3520 the M1 draft in the editor, 4 fields checked with one rule(10) partial: 1 right
4.1 MR !3820 the reference in the editor, the same 4 fields: 4 right
M1 collects every caller's arguments in one place (step 3), so after its first result each later call of rule adds to a full array. M2 drops what comes after the arity (step 6), which a function reading arguments or a rest parameter notices.
4.2 Streaming: one partial, three titles
A video player builds its stream requests with the reference as request = curry((quality, title, startAt) => …), and keeps const hd = request('1080p') for every title on the shelf:
4.2 hd('Intro')(0) -> 1080p/Intro@0 · hd('Pilot')(90) -> 1080p/Pilot@90 · hd('Finale', 30) -> 1080p/Finale@30
4.2 hd('Extra')(5) after those three -> 1080p/Extra@5 (hd still holds one argument)
Each call of hd builds a new array from ['1080p'], so the three requests never see each other's titles (step 4), and hd itself still holds one argument after them.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 8
A streaming app passes these three handlers to curry, which reads each one's fn.length as its arity:
Put the handlers in order of the length each reports, smallest first.
Order these chips: function (show, season) {} function (...episodes) {} function (show, season = 1, episode) {}
const handlers = [ function (...episodes) {}, function (show, season = 1, episode) {}, function (show, season) {},];Your order
- 1empty
- 2empty
- 3empty
Tap the lines in the order you expect.
Question 2 of 8
ExamHall's validator factory needs a curry. A candidate's kata answer is complete apart from one line:
The team's list has 9 tests, and each calls a three-argument function through curry.
Which line passes all 9?
export function curry(fn) { return function curried(...args) { if (args.length >= fn.length) return fn(...args); /* the line */ };}Question 3 of 8
A chat client curries its send function with this kata answer:
The composer passes its options object as a fourth argument: send('general')('ada', 'hi', { silent: true }).
What does that call do?
export function curry(fn) { return function curried(...args) { if (args.length === fn.length) return fn(...args); return (...more) => curried(...args, ...more); };} export const send = curry((room, user, text) => `${room}/${user}: ${text}`);Question 4 of 8
A video player builds its stream choice with the kata's reference curry:
The player calls pickStream('E1')('hd')('fr').
What happens?
import { curry } from './curry.mjs'; export const pickStream = curry((title, quality = 'auto', subtitles) => `${title}/${quality}/${subtitles}`, 3);Question 5 of 8
the interviewer says: "Implement curry(fn). f(1)(2)(3), f(1, 2)(3) and f(1, 2, 3) should all work, and a partial like add1 = f(1) must be reusable." You have 15 minutes and a plain editor, and ExamHall's rule(max)(field)(value) is the use case.
Before you type: say your test list out loud.
Name at least six tests, each with its calls and what it expects.
Question 6 of 8
the interviewer says: "Implement curry(fn). f(1)(2)(3), f(1, 2)(3) and f(1, 2, 3) should all work, and a partial like add1 = f(1) must be reusable." You have 15 minutes and a plain editor, and ExamHall's rule(max)(field)(value) is the use case.
Now write it, in 15 minutes, from a blank file that exports curry.
Question 7 of 8
A shop labels its price tags per region with this kata answer:
The page runs const eu = label('eu'), then eu('A1'), eu('B2') and eu(35), in that order.
What does the last call return?
export function curry(fn) { return function curried(...args) { if (args.length >= fn.length) return fn(...args); return (...more) => { args.push(...more); return curried(...args); }; };} export const label = curry((shop, sku, price) => `${shop}:${sku}=${price}`);Question 8 of 8
A payments ledger curries its posting function with this kata answer:
The ledger runs const cash = post('cash'), then const rent = cash(100), then rent('rent'), then cash(5).
What does the last call return?
export function curry(fn) { return function curried(...args) { if (args.length >= fn.length) return fn(...args); return (...more) => { const all = args; all.push(...more); return curried(...all); }; };} export const post = curry((account, amount, memo) => `${account}:${amount}:${memo}`);Go deeper
Pro tips
- Run the reuse test yourself before the interviewer does: call
add1(2)(3), thenadd1(10)(20), on one partial. When: as soon asf(1)(2)(3)passes; a draft that passes one chain can still fail the second (step 10). Verify (RAN,tips.mjs): the list run on the shared-array mutant. (The curriculum row's tip, reworded so it states no item's answer; its test numbers were not this list's.)tip 1 the shared-array mutant (one array per curry() call): 5 of 9 pass · red: C2, C5, C6, C7 - Run your own answer against the list before you call it done:
KATA_IMPL=./my-curry.mjs node --test kata-core.test.mjs. When: practising the kata at home, against the same tests the reference passes (step 10). Verify (RAN,tips.mjs):tip 2 KATA_IMPL=./reference.mjs: exit 0 · tests 9 · pass 9 · fail 0 tip 2 KATA_IMPL=<temp>/m1-curry.mjs (one array per curry() call): exit 1 · tests 9 · pass 5 · fail 4 · red: C2, C5, C6, C7
Source
Vendor and guide pages change, so their claims are labelled as of 2026-10-12; every behaviour this lesson grades is executed on Node 24.
- MDN, Function: length (the curriculum row's first source), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length (
mdn-function-length.txt): "Only parameters before the first one with a default value are counted." and "The rest parameter is excluded.". As of 2026-10-12. - Lodash 4.17.15 documentation,
_.curry(the row's second source), https://lodash.com/docs/4.17.15#curry. lodash.com refused connections on 2026-10-12 (curl exit 28, at 12:25Z and on the retry), so the page was read from the unmodified Internet Archive capture (id_, Memento-Datetime 2025-12-05T10:14:29Z) of that URL, as_fix-brief-r1.mdruling 1 accepts for a named primary page (lodash-curry.txt, header linesVia,Memento-DatetimeandRaw-SHA256). It says curry "either invokes func returning its result, if at least arity number of arguments have been provided, or returns a function that accepts the remaining func arguments", and "The arity of func may be specified if func.length is not sufficient." (steps 5–6, 8). Lodash keys no item; this lesson's reference is its own code. - Front End Interview Handbook, JavaScript coding, https://www.frontendinterviewhandbook.com/coding/javascript-utility-function (
feih-javascript-coding.txt): candidates take "around 10-15 minutes for a basic question" (the time box, step 1), and it advises "writing test cases" (step 10). As of 2026-10-12.
Related lessons
Checked against the tools and docs as of 2026-10-12.