Skip to content

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?

Code for this question
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'}`);
Pick one

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

Ask, then say the contract. 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) {}

Code for this question
const handlers = [  function (...episodes) {},  function (show, season = 1, episode) {},  function (show, season) {},];

Your order

  1. 1empty
  2. 2empty
  3. 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?

Code for this question
export function curry(fn) {  return function curried(...args) {    if (args.length >= fn.length) return fn(...args);    /* the line */  };}
Pick one

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?

Code for this question
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}`);
Pick one

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?

Code for this question
import { curry } from './curry.mjs'; export const pickStream = curry((title, quality = 'auto', subtitles) => `${title}/${quality}/${subtitles}`, 3);
Pick one

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?

Code for this question
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}`);
Pick one

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?

Code for this question
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}`);
Pick one

Go deeper

Pro tips
  1. Run the reuse test yourself before the interviewer does: call add1(2)(3), then add1(10)(20), on one partial. When: as soon as f(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
  2. 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.md ruling 1 accepts for a named primary page (lodash-curry.txt, header lines Via, Memento-Datetime and Raw-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.

Checked against the tools and docs as of 2026-10-12.