pipe()
intermediateCompose functions left-to-right into a pipeline — the one-liner with a lot of interview depth.
Implement curry with placeholder support in JavaScript — Symbol sentinels, fill-then-append merging, and the completion rule that most implementations get wrong.
Extend curry with a placeholder
_so arguments can arrive out of order:curriedSubtract(10, _, 5)(3)fills the hole →subtract(10, 3, 5).
This is the standard hard follow-up to currying, and it's genuinely trickier than it looks — most blog implementations (and lodash-cargo-cult versions with a string '_') contain one or both of two real bugs. Solving it cleanly requires stating two rules precisely:
fn.length positions are filled with no placeholder among them. Counting non-placeholder args isn't enough — where they sit matters.const _ = Symbol("placeholder"); // a Symbol can't collide with real data
function curry(fn) {
return function curried(...args) {
const needed = args.slice(0, fn.length);
const complete = needed.length >= fn.length && !needed.includes(_);
if (complete) {
return fn.apply(this, needed);
}
return function (...nextArgs) {
// Rule 1: fill placeholders left-to-right, then append leftovers
const merged = [];
let n = 0;
for (const arg of args) {
merged.push(arg === _ && n < nextArgs.length ? nextArgs[n++] : arg);
}
merged.push(...nextArgs.slice(n));
return curried.apply(this, merged);
};
};
}const subtract = (a, b, c) => a - b - c;
const cs = curry(subtract);
cs(10, _, 5)(3);
// call 1: args=[10, _, 5] → needed has a placeholder → not complete
// call 2: nextArgs=[3] → merge: 10 stays, _ ← 3, 5 stays → [10, 3, 5]
// needed=[10,3,5], no placeholder, length 3 → subtract(10,3,5) = 2 ✓
cs(_, 3)(10, 5); // merge: _ ← 10, 3 stays, append 5 → [10,3,5] → 2
cs(10)(_, 5)(3); // [10] → [10,_,5] → fill → [10,3,5] → 2
cs(_, _, 5)(_, 3)(10); // placeholders can even be re-supplied as placeholders:
// [_,_,5] + [_,3] → [_,3,5] + [10] → [10,3,5] → 2That last case — a placeholder filled by another placeholder — falls out of the merge rule for free, and it's the input interviewers use to break over-clever implementations.
Bug 1: filtering placeholders before calling. The common broken version does validArgs = args.filter(a => a !== _) and, on completion, calls fn(...validArgs) — which collapses positions. For cs(10, _, 5, 3), filtering gives subtract(10, 5, 3); the placeholder said the 3 belongs in the middle: subtract(10, 3, 5). Cruelly, with 10 - x - y both orders give 2, so the broken version passes its own example — the perfect demonstration of why test cases must target the semantics (use (a - b) * c-style non-commutative functions), not just happy outputs.
Bug 2: counting instead of positioning in the completion check. cs(_, 2, 3, 4) for arity 3 has three non-placeholder args, so a count-based check calls subtract(2, 3, 4) — silently ignoring that position a was explicitly deferred. The positional rule (no placeholder within the first fn.length slots) refuses to run until the hole is filled: a later (1) merges to [1, 2, 3, 4] and calls subtract(1, 2, 3).
And the sentinel choice is a third, quieter trap: a string '_' placeholder means the data value "_" can never be passed as a real argument. A Symbol is unforgeable — nothing in user data can equal it. (Lodash uses the lodash function object itself as the sentinel — same unforgeability idea.) Also note merged.includes(_)-style checks: includes uses SameValueZero, so a stray NaN argument doesn't confuse anything — a nice tie-in to Object.is semantics.
slice(0, fn.length) at invoke time (they existed only to fill holes); state that policy.cs(_, _, _) never completes until real values arrive; each call just re-merges.fn.length fragility — defaults and rest params undercount arity, inherited from plain curry; the explicit-arity parameter fix applies here identically.this forwarding — same as base curry: function collectors + apply(this, ...).merged is built fresh per call; sibling partials sharing a prefix must not see each other's fills.'_', null, undefined — the last also breaks passing legitimate undefined).nextArgs with shift() mid-map — works until someone edits it; the index-cursor loop states the algorithm honestly.partial with placeholders instead." — one merge pass, no recursion or arity check — the simpler cousin; articulating what disappears shows you see the structure.R.__) and lodash (_.curry with the lodash placeholder); knowing placeholders are a shipped, used feature — mainly for slotting data into the last position of config-first APIs — grounds the exercise.Compose functions left-to-right into a pipeline — the one-liner with a lot of interview depth.
Transform f(a, b, c) into f(a)(b)(c): the classic closure exercise, with arity handling.
Pre-fill leading arguments of a function — partial application, and how it differs from currying.
Guarantee a function runs exactly once and returns its cached result forever after.