Lesson 16·M2 Data & Functions
Currying & Partial Application
OpenLearning Objectives
By the end of this lesson you should be able to:
- Explain the difference between currying and partial application without looking it up
- Write
curry()from scratch and know the order-argument gotchas - Recognise curried APIs (
connect(), Express middleware,fetchwrappers) in the wild - Say exactly when currying helps and when it's needless indirection
One-line Definition
Currying turns a function that takes multiple arguments into a chain of functions that each take one argument.
const add = (a, b, c) => a + b + c;
const curried = (a) => (b) => (c) => a + b + c;
console.log(add(1, 2, 3), curried(1)(2)(3));
Output:
6 6
Same result, same math — different calling convention. That's the whole idea.
Mental Model
A curried function is an assembly line.
curried(1) → worker that "knows" a = 1
curried(1)(2) → worker that "knows" a = 1, b = 2
curried(1)(2)(3) → finished product: 6
Each call returns a partially-configured worker, and each worker captures the arguments that already arrived. Currying is just closures (Lesson 5) applied to a calling convention.
Visual Flow
curried(1)(2)(3)
┌─────────────────────────────────────────────────┐
│ (a) => (b) => (c) => a + b + c │
│ │
│ curried(1) → (b) => (c) => 1 + b + c a captured
│ curried(1)(2) → (c) => 1 + 2 + c a, b captured
│ curried(1)(2)(3) → 1 + 2 + 3 = 6 done
└─────────────────────────────────────────────────┘
Each step eats one argument and hands the rest to a new closure. No step touches the arguments that haven't arrived yet.
How It Works
Every arrow layer creates a scope holding the arguments passed so far (the closure mechanism from Lesson 5). The inner layers read the outer ones through the scope chain:
const add3 = (a) => (b) => (c) => a + b + c;
console.log(add3(10)(20)(30));
Output:
60
The generic curry wrapper — the classic live-coding answer:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...more) => curried(...args, ...more);
};
}
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3));
console.log(curriedAdd(1, 2)(3));
console.log(curriedAdd(1, 2, 3));
Output:
6
6
6
fn.length is the number of declared parameters — that's how the wrapper knows when enough
arguments have arrived. Collect what comes in, and once the count reaches fn.length, call
the original. All three call shapes work.
Real Project Usage
The idea is everywhere once you can see it:
| API | Shape | The pattern |
|---|---|---|
Redux connect(mapState, mapDispatch) | connect(...)(Component) | Configure, then apply |
| Express middleware | (req, res, next) => … | One factory per route stack |
| Loggers | createLogger('info')(msg) | Bind the level, reuse the rest |
| Request wrappers | requestWith(baseUrl)(path) | Fix the host, vary the path |
React useState | useState(0) returns [value, setValue] | Configuring a value you'll change later |
connect is the most quoted example in interviews. It takes the pieces you configure once —
mapping functions, options — and returns a second function that you apply to each component:
const withUser = connect((state) => ({ user: state.user }));
const UserCard = withUser(Profile);Interview Explanation
Currying turns
f(a, b, c)intof(a)(b)(c)— each call returns a function waiting for the next argument. Partial application fixes some arguments ahead of time and leaves the rest, likebind(null, 1, 2).I use it for configuration: build once with the fixed pieces, then apply to many targets — that's
connect()and middleware.
Senior-Level Insights
- Currying is a tool for reuse with configuration, not a performance trick. The senior answer names when the pattern pays: one shared fixed input applied to many varying inputs.
- Arity is the design decision. Order arguments "data last" so partial application reads
naturally:
map(fn)(list)instead ofmap(list)(fn). Most utility libraries order the fixed part first and the data last. - Currying never mutates or caches. It's pure composition (Lesson 14) — the captured arguments are read-only, so the whole chain is safe to share.
- Know the honest critique. For one-off calls, curried syntax is slower and harder to
read. It earns its keep in reusable, configured pipelines — not in a single
addcall.
Common Mistakes
Mistake 1 — currying a fixed-arity function "works", but fn.length bites.
function addAll(...nums) { // rest parameter
return nums.reduce((t, n) => t + n, 0);
}
console.log(addAll.length); // 0 — no declared params
Output:
0
A rest parameter makes length zero, so a generic curry that relies on fn.length will
fire on the first call with whatever it got. Say it in an interview: rest parameters break
arity-based currying.
Mistake 2 — confusing currying with partial application.
const add = (a, b) => a + b;
const with5 = add.bind(null, 5); // partial application, not currying
console.log(with5(10)); // add(5, 10)
Output:
15
bind fixes one argument and leaves the rest. Currying converts the function's shape —
each call accepts exactly one argument. Different mechanisms, related goals.
Mistake 3 — calling a curried function with too many arguments at once if you didn't
write the wrapper to tolerate it. The chain shape (a) => (b) => … drops extras silently.
Best Practices
✅ Order arguments configuration-first, data-last, so partial application reads naturally
✅ Use curry(fn) with a real arity check (fn.length) — never with rest-parameter functions
✅ Reach for the pattern when one fixed input is applied to many targets (middleware, connect)
✅ Keep curried chains short — two or three layers is plenty
❌ Don't curry for a single call site; the indirection isn't free to read
❌ Don't rely on this inside curried functions (the arrow layers drop it)
Interview Questions
Q1. What is currying?
Converting a multi-argument function into a chain of single-argument functions.
f(a, b, c)becomesf(a)(b)(c), where each step returns a function waiting for the next argument.
Q2. Currying vs partial application?
Currying changes the function's shape — it returns a function per argument. Partial application fixes some arguments and leaves the rest for later, like
bind(null, 1). A curried function can be partially applied naturally, but they aren't the same thing.
Q3. Implement curry.
Recursively collect arguments until we have
fn.lengthof them, then call the original:
function curry(fn) {
return function curried(...args) {
return args.length >= fn.length
? fn(...args)
: (...more) => curried(...args, ...more);
};
}
It relies on
fn.length, which is why currying functions with default or rest parameters is unreliable.
Senior follow-up: How is connect() currying?
connect(mapState, mapDispatch)returns a function, and you apply that function to a component:connect(...)(Component). That's the configure-then-apply shape — the configuration is fixed once, then reused across many components.
Senior follow-up: When would you not use currying?
For a one-off call it's slower and harder to read. I reach for it when the same fixed configuration is applied repeatedly — a middleware stack or a wrapped API client. Otherwise a plain function is the better tool.
Follow-up Questions
Why does fn.length matter to a curry implementation?
It's the signal for "enough arguments". The wrapper compares the collected count against it and calls the original function once they match.
lengthcounts declared parameters and stops at the first default/rest parameter — so those functions can't be curried reliably.
Can you partially apply without bind?
Yes — a closure (Lesson 5):
const withBase = (base) => (n) => base + n;
const addTax = withBase(1.15); // price * 1.15, base fixed
The factory closes over
base.binddoes the same thing withthisplus leading arguments; the closure does it withoutthis.
Comparison Table
| Currying | Partial Application | |
|---|---|---|
| Changes function shape | ✅ f(a,b,c) → f(a)(b)(c) | ❌ keeps arity |
| Fixes some args | Individually, one per step | ✅ all at once |
| Primary tool | curry() | fn.bind(null, …) / closure |
| Example | curriedAdd(1)(2)(3) | const double = n => n * 2 |
| Typical use | connect(), middleware | Configuration, wrappers |
Code Example
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const double = (n) => n * 2;
const square = (n) => n * n;
console.log(compose(square, double)(5)); // square(double(5))
Output:
100
compose is the other half of the functional vocabulary — right-to-left, like math notation
f ∘ g. A curried map slots straight into it:
const curriedMap = (fn) => (arr) => arr.map(fn);
const addTax = (n) => Math.round(n * 1.15);
console.log(curriedMap(addTax)([100, 200]));
Output:
[ 115, 230 ]Performance Notes
- Currying is not an optimisation. Each layer adds a closure allocation and a function call. The overhead is negligible per call — it only shows up in hot loops where the extra indirection compounds.
- Where it pays is reuse, not speed. The win is one configured function reused across a codebase, not faster execution.
- If you need speed and currying, partially apply once, reuse the result — don't re-curry inside the hot path. As always, measure before you "optimise".
Debugging Scenarios
Scenario 1 — "It returned a function instead of the answer."
The curry from section 4, self-contained so the block runs standalone:
const add = (a, b, c) => a + b + c;
const curriedAdd = (a) => (b) => (c) => a + b + c;
console.log(curriedAdd(1, 2)); // fewer args than the chain needs
console.log(curriedAdd(1)(2)); // same — still waiting for (c)
Output:
[Function (anonymous)]
[Function (anonymous)]
You passed fewer arguments than fn.length, so you got the collector back. Either keep
calling — curriedAdd(1, 2)(3) — or pass everything at once.
Scenario 2 — curriedAdd(1)(2) returns NaN.
const add = (a, b, c = 0) => a + b + c; // default param
console.log(add.length); // 2 — stops at the default
console.log(add(1, 2)); // 3 — c fell back to its default
Output:
2
3
The default parameter shrank fn.length to 2, so the wrapper fired early with only two
arguments. c was undefined at call time (here it fell back to the default 0). Fix:
drop the default, or curry by an explicit arity instead of fn.length.
Scenario 3 — a curried wrapper returns a function forever.
const addAll = (...nums) => nums.reduce((t, n) => t + n, 0);
fn.length is 0, so the first call already has "enough" arguments and the recursion stops
immediately. Currying a variadic function needs a manual arity — this is a design problem,
not a bug in your code.
Quick Revision Notes
- Currying =
f(a)(b)(c); partial application = fixing arguments, leaving the rest - A curried function is a chain of closures, each capturing the arguments so far
curry(fn)collects untilargs.length >= fn.length, then callsfnfn.lengthcounts declared params and stops at the first default/rest parameter- Order arguments configuration-first, data-last
connect(...)(Component)and Express middleware are curried-style APIs- Currying is about reuse, not speed
Cheat Sheet
curry(fn) → curriedFn, applies once args.length >= fn.length
curry(add)(1)(2)(3) === add(1, 2, 3)
partial → fix some args now (bind / closure), call with the rest later
fn.length → declared params, broken by defaults and rest
compose(...fns) → right-to-left pipeline: compose(square, double)(5) === 100Key Takeaways
Recap
- Currying converts a multi-argument function into a chain of single-argument functions
- Each step is a closure capturing the arguments passed so far (Lesson 5)
- Partial application fixes some arguments and leaves the rest — a different, related idea
curryhinges onfn.length, which default and rest parameters silently break- The pattern is configuration-then-application:
connect(), middleware, loggers - It's a reuse tool, not a speed tool — reach for it when one config serves many targets
Check your understanding
Answer each one out loud. Then rate yourself honestly.
0/5Distinguish currying from partial application in one sentence each.
Why is fn.length the natural arity signal — and when does it lie?
Walk through what curried(1)(2) returns and what it holds, in terms of closures.
How is Redux's connect() an example of currying?
What breaks if you curry a function with default parameters?
What's Next
Lesson 17 — Memoization. Take a pure function, add a cache, and turn repeated work into
a hash lookup — then see how React's useMemo is the same idea wearing a hook.