AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

11 Functional patterns / Lesson 2 of 5

Composition, currying and point-free style — building programs from small functions

13 min · +10 XP · 2 exercises · 5 questions

Read the text13 min
Exercises0/2
Quiz5 q

Quick answer

What is currying in JavaScript?

Currying transforms a function of several arguments into a chain of functions that each take one, so add(a, b, c) becomes add(a)(b)(c) and every call fixes one argument. A generic curry helper collects arguments until it has as many as fn.length, then calls the function. Currying produces the one-argument steps that pipe and compose chain together.

Once functions are pure, they compose: the output of one is the input of the next, and a program becomes a pipeline of small, named, testable steps. Two mechanical tools make that pleasant in JavaScript — pipe/compose to chain unary functions, and currying/partial application to turn multi-argument helpers into the unary pieces a pipeline wants — plus one design convention, data-last, that decides whether a library composes well. This lesson builds the tools, shows the style they enable (including "point-free" code with no named parameters), compares it honestly with method chaining, and marks the places where the style stops helping.

pipe and compose

const pipe = (...fns) => (x) => fns.reduce((acc, f) => f(acc), x);        // left to right: pipe(f, g)(x) = g(f(x))
const compose = (...fns) => (x) => fns.reduceRight((acc, f) => f(acc), x); // right to left: compose(f, g)(x) = f(g(x))

const slugify = pipe(
  (s) => s.trim().toLowerCase(),
  (s) => s.replace(/[^a-z0-9]+/g, "-"),
  (s) => s.replace(/^-|-$/g, ""),
);
slugify("  Hello, World! ");   // "hello-world"

pipe reads in execution order and is what most JavaScript code uses; compose matches mathematical notation. Both work only with unary functions — each step takes one value — which is why the next two tools exist. A pipeline is a value: store it, pass it, test each step on its own, insert a tap ((x) => { log(x); return x; }) to inspect the middle.

Currying and partial application

const curry = (fn) => function curried(...args) {
  return args.length >= fn.length ? fn(...args) : (...more) => curried(...args, ...more);
};
const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); add(1, 2)(3); add(1)(2, 3);   // 6 — all shapes work

const partial = (fn, ...preset) => (...later) => fn(...preset, ...later);
const greetAda = partial(greet, "Hello", "Ada");

Currying transforms f(a, b, c) into f(a)(b)(c) — a chain of unary functions, where each call fixes one argument; partial application fixes some arguments now and takes the rest later, in one call. Auto-currying (above) accepts arguments in any grouping by comparing against fn.length — which is why it breaks on functions with default or rest parameters (they do not count toward length) and on variadic functions; state the arity explicitly (curryN(3, fn)) in those cases.

Data-last

const map = curry((fn, xs) => xs.map(fn));
const filter = curry((pred, xs) => xs.filter(pred));
const sortBy = curry((key, xs) => [...xs].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)));
const take = curry((n, xs) => xs.slice(0, n));

const topAdults = pipe(filter((u) => u.age >= 18), sortBy("age"), map((u) => u.name), take(3));
topAdults(users);

Put the configuration first and the data last: map(fn, xs) not map(xs, fn). Then partially applying the configuration yields exactly the unary function a pipeline needs, and the same topAdults works on any array. Lodash's _.map(xs, fn) is data-first (chain-friendly, lodash/fp flips it); Ramda and lodash/fp are data-last by design. If you write utilities meant to compose, make them data-last and curried.

Point-free style

"Point-free" (tacit) code names no arguments: const names = map(prop("name")) instead of (users) => users.map((u) => u.name). It removes noise when the functions have good names — pipe(trim, toLower, slugify) — and adds a puzzle when they do not: compose(map(compose(add(1), multiply(2))), filter(complement(isNil))) reads worse than a two-line arrow. Use it for straight pipelines of well-named steps; write the arrow when an argument name would help the reader. Array.prototype.map(parseInt) is the cautionary tale: point-free with a function that takes more arguments than you think.

Small combinators worth having

const identity = (x) => x;
const constant = (v) => () => v;
const tap = (f) => (x) => { f(x); return x; };
const prop = (k) => (o) => o[k];
const not = (pred) => (...a) => !pred(...a);
const flip = (f) => (a, b) => f(b, a);
const unary = (f) => (x) => f(x);                // fixes .map(parseInt)
const juxt = (...fns) => (x) => fns.map((f) => f(x));   // apply several functions to one value
const once = (f) => { let done = false, r; return (...a) => (done ? r : ((done = true), (r = f(...a)))); };

Each is five to ten characters of logic and saves an inline arrow at every use.

Pipelines versus method chains

xs.filter(p).map(f).slice(0, 3) is already a pipeline — for arrays, chaining is idiomatic and often clearer, and it needs no library. pipe wins when the steps are not array methods (string transforms, domain functions, validation), when you want to reuse the composed function, when steps come from different modules, or when the data is not an array (a single record, a promise via pipeP/async steps). Many codebases use both: method chains inside steps, pipe between steps. JavaScript's pipeline operator (|>) remains a proposal.

Async composition

const pipeAsync = (...fns) => (x) => fns.reduce((p, f) => p.then(f), Promise.resolve(x)); — each step may be sync or async; the result is a promise. Error handling is the promise chain's: one catch at the end, or a Result-returning style (lesson 5).

When to stop

Composition shines for transformations of data: parsing, normalising, filtering, reporting. It fights you for stateful, event-driven, or performance-critical code — a game loop, a socket handler, a tight numeric kernel — where a plain loop with mutable locals is clearer and faster. A curried, point-free, five-combinator expression that takes a minute to decode has failed the reader; the test of a functional style is that the next developer finds it obvious.

Common mistakes

  • Composing non-unary functions; compose when pipe order was intended.
  • Auto-curry on functions with defaults/rest parameters (length is wrong) or on variadic functions.
  • Data-first helpers in a pipeline (nothing partially applies cleanly).
  • Point-free with functions that take extra arguments (map(parseInt)).
  • Rewriting an obvious for loop into a compose tower for style points.
  • Forgetting that a pipeline of arrays allocates per step — fine normally, measure on hot paths.

Interview angle

  • "Implement pipe and compose." reduce/reduceRight over the functions with the value as accumulator.
  • "Currying versus partial application?" Currying turns an n-ary function into a chain of unary calls; partial application fixes some arguments in one call.
  • "Why data-last?" So partially applying the configuration yields a unary function that composes.
  • "What is point-free style, and when is it bad?" Code without named arguments; bad when the combinator soup is harder to read than an arrow.
  • "How does curry know when to call the function?" It compares collected arguments to fn.length — which fails for defaults, rest and variadics.

Key takeaways

  • pipe (left→right) and compose (right→left) chain unary functions with reduce; pipelines are values you can store, reuse and tap.
  • Curry to get unary steps from multi-argument helpers; partial application fixes leading arguments; watch fn.length.
  • Data-last, curried utilities compose; data-first ones chain.
  • Point-free for clean pipelines of well-named functions; arrows when a parameter name helps.
  • Use composition for data transformation; loops and mutable locals for stateful or hot code.

Common questions

What is the difference between currying and partial application?

Currying turns an n-argument function into a chain of one-argument calls. Partial application fixes some arguments now, in one call, and returns a function that takes the rest later: partial(greet, "Hello", "Ada") returns a function awaiting the remaining arguments. Both produce specialised functions from general ones.

How do you implement pipe and compose in JavaScript?

Both fold over the functions with the value as the accumulator. pipe runs left to right, (...fns) => (x) => fns.reduce((acc, f) => f(acc), x), so pipe(f, g)(x) is g(f(x)). compose uses reduceRight and runs right to left, matching mathematical notation. Both expect one-argument functions.

What is point-free style in JavaScript?

Point-free, or tacit, style defines functions without naming their arguments, such as const names = map(prop("name")) instead of an arrow that takes users. It reads well for a pipeline of well-named steps and badly when it turns into nested combinators; ["1", "2", "3"].map(parseInt) returning [1, NaN, NaN] is the cautionary tale.

Why does auto-curry break with default parameters?

A generic curry decides when to call the function by comparing the arguments collected with fn.length, and length stops counting at the first parameter with a default value or a rest parameter. The function is then called too early. State the arity explicitly instead, as a curryN(3, fn) helper does.

What does data-last mean in functional JavaScript?

Data-last means a utility takes its configuration first and the data it operates on last, map(fn, xs) rather than map(xs, fn). Partially applying the configuration then yields exactly the one-argument function a pipeline needs. Ramda and lodash/fp are data-last by design; plain Lodash is data-first and suits chaining.

Mark as readSign in to keep your place.

Currying in JavaScript: Pipe, Compose and Point-Free Style

Currying turns f(a, b, c) into f(a)(b)(c). Build pipe and compose with reduce, partial application and data-last helpers, and see when point-free style hurts.

What is currying in JavaScript?

Currying transforms a function of several arguments into a chain of functions that each take one, so add(a, b, c) becomes add(a)(b)(c) and every call fixes one argument. A generic curry helper collects arguments until it has as many as fn.length, then calls the function. Currying produces the one-argument steps that pipe and compose chain together.

Lesson

Once functions are pure, they compose: the output of one is the input of the next, and a program becomes a pipeline of small, named, testable steps. Two mechanical tools make that pleasant in JavaScript — pipe/compose to chain unary functions, and currying/partial application to turn multi-argument helpers into the unary pieces a pipeline wants — plus one design convention, data-last, that decides whether a library composes well. This lesson builds the tools, shows the style they enable (including "point-free" code with no named parameters), compares it honestly with method chaining, and marks the places where the style stops helping.

pipe and compose

const pipe = (...fns) => (x) => fns.reduce((acc, f) => f(acc), x);        // left to right: pipe(f, g)(x) = g(f(x))
const compose = (...fns) => (x) => fns.reduceRight((acc, f) => f(acc), x); // right to left: compose(f, g)(x) = f(g(x))

const slugify = pipe(
  (s) => s.trim().toLowerCase(),
  (s) => s.replace(/[^a-z0-9]+/g, "-"),
  (s) => s.replace(/^-|-$/g, ""),
);
slugify("  Hello, World! ");   // "hello-world"

pipe reads in execution order and is what most JavaScript code uses; compose matches mathematical notation. Both work only with unary functions — each step takes one value — which is why the next two tools exist. A pipeline is a value: store it, pass it, test each step on its own, insert a tap ((x) => { log(x); return x; }) to inspect the middle.

Currying and partial application

const curry = (fn) => function curried(...args) {
  return args.length >= fn.length ? fn(...args) : (...more) => curried(...args, ...more);
};
const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); add(1, 2)(3); add(1)(2, 3);   // 6 — all shapes work

const partial = (fn, ...preset) => (...later) => fn(...preset, ...later);
const greetAda = partial(greet, "Hello", "Ada");

Currying transforms f(a, b, c) into f(a)(b)(c) — a chain of unary functions, where each call fixes one argument; partial application fixes some arguments now and takes the rest later, in one call. Auto-currying (above) accepts arguments in any grouping by comparing against fn.length — which is why it breaks on functions with default or rest parameters (they do not count toward length) and on variadic functions; state the arity explicitly (curryN(3, fn)) in those cases.

Data-last

const map = curry((fn, xs) => xs.map(fn));
const filter = curry((pred, xs) => xs.filter(pred));
const sortBy = curry((key, xs) => [...xs].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0)));
const take = curry((n, xs) => xs.slice(0, n));

const topAdults = pipe(filter((u) => u.age >= 18), sortBy("age"), map((u) => u.name), take(3));
topAdults(users);

Put the configuration first and the data last: map(fn, xs) not map(xs, fn). Then partially applying the configuration yields exactly the unary function a pipeline needs, and the same topAdults works on any array. Lodash's _.map(xs, fn) is data-first (chain-friendly, lodash/fp flips it); Ramda and lodash/fp are data-last by design. If you write utilities meant to compose, make them data-last and curried.

Point-free style

"Point-free" (tacit) code names no arguments: const names = map(prop("name")) instead of (users) => users.map((u) => u.name). It removes noise when the functions have good names — pipe(trim, toLower, slugify) — and adds a puzzle when they do not: compose(map(compose(add(1), multiply(2))), filter(complement(isNil))) reads worse than a two-line arrow. Use it for straight pipelines of well-named steps; write the arrow when an argument name would help the reader. Array.prototype.map(parseInt) is the cautionary tale: point-free with a function that takes more arguments than you think.

Small combinators worth having

const identity = (x) => x;
const constant = (v) => () => v;
const tap = (f) => (x) => { f(x); return x; };
const prop = (k) => (o) => o[k];
const not = (pred) => (...a) => !pred(...a);
const flip = (f) => (a, b) => f(b, a);
const unary = (f) => (x) => f(x);                // fixes .map(parseInt)
const juxt = (...fns) => (x) => fns.map((f) => f(x));   // apply several functions to one value
const once = (f) => { let done = false, r; return (...a) => (done ? r : ((done = true), (r = f(...a)))); };

Each is five to ten characters of logic and saves an inline arrow at every use.

Pipelines versus method chains

xs.filter(p).map(f).slice(0, 3) is already a pipeline — for arrays, chaining is idiomatic and often clearer, and it needs no library. pipe wins when the steps are not array methods (string transforms, domain functions, validation), when you want to reuse the composed function, when steps come from different modules, or when the data is not an array (a single record, a promise via pipeP/async steps). Many codebases use both: method chains inside steps, pipe between steps. JavaScript's pipeline operator (|>) remains a proposal.

Async composition

const pipeAsync = (...fns) => (x) => fns.reduce((p, f) => p.then(f), Promise.resolve(x)); — each step may be sync or async; the result is a promise. Error handling is the promise chain's: one catch at the end, or a Result-returning style (lesson 5).

When to stop

Composition shines for transformations of data: parsing, normalising, filtering, reporting. It fights you for stateful, event-driven, or performance-critical code — a game loop, a socket handler, a tight numeric kernel — where a plain loop with mutable locals is clearer and faster. A curried, point-free, five-combinator expression that takes a minute to decode has failed the reader; the test of a functional style is that the next developer finds it obvious.

Common mistakes

  • Composing non-unary functions; compose when pipe order was intended.
  • Auto-curry on functions with defaults/rest parameters (length is wrong) or on variadic functions.
  • Data-first helpers in a pipeline (nothing partially applies cleanly).
  • Point-free with functions that take extra arguments (map(parseInt)).
  • Rewriting an obvious for loop into a compose tower for style points.
  • Forgetting that a pipeline of arrays allocates per step — fine normally, measure on hot paths.

Interview angle

  • "Implement pipe and compose." reduce/reduceRight over the functions with the value as accumulator.
  • "Currying versus partial application?" Currying turns an n-ary function into a chain of unary calls; partial application fixes some arguments in one call.
  • "Why data-last?" So partially applying the configuration yields a unary function that composes.
  • "What is point-free style, and when is it bad?" Code without named arguments; bad when the combinator soup is harder to read than an arrow.
  • "How does curry know when to call the function?" It compares collected arguments to fn.length — which fails for defaults, rest and variadics.

Key takeaways

  • pipe (left→right) and compose (right→left) chain unary functions with reduce; pipelines are values you can store, reuse and tap.
  • Curry to get unary steps from multi-argument helpers; partial application fixes leading arguments; watch fn.length.
  • Data-last, curried utilities compose; data-first ones chain.
  • Point-free for clean pipelines of well-named functions; arrows when a parameter name helps.
  • Use composition for data transformation; loops and mutable locals for stateful or hot code.

Common questions

What is the difference between currying and partial application?

Currying turns an n-argument function into a chain of one-argument calls. Partial application fixes some arguments now, in one call, and returns a function that takes the rest later: partial(greet, "Hello", "Ada") returns a function awaiting the remaining arguments. Both produce specialised functions from general ones.

How do you implement pipe and compose in JavaScript?

Both fold over the functions with the value as the accumulator. pipe runs left to right, (...fns) => (x) => fns.reduce((acc, f) => f(acc), x), so pipe(f, g)(x) is g(f(x)). compose uses reduceRight and runs right to left, matching mathematical notation. Both expect one-argument functions.

What is point-free style in JavaScript?

Point-free, or tacit, style defines functions without naming their arguments, such as const names = map(prop("name")) instead of an arrow that takes users. It reads well for a pipeline of well-named steps and badly when it turns into nested combinators; ["1", "2", "3"].map(parseInt) returning [1, NaN, NaN] is the cautionary tale.

Why does auto-curry break with default parameters?

A generic curry decides when to call the function by comparing the arguments collected with fn.length, and length stops counting at the first parameter with a default value or a rest parameter. The function is then called too early. State the arity explicitly instead, as a curryN(3, fn) helper does.

What does data-last mean in functional JavaScript?

Data-last means a utility takes its configuration first and the data it operates on last, map(fn, xs) rather than map(xs, fn). Partially applying the configuration then yields exactly the one-argument function a pipeline needs. Ramda and lodash/fp are data-last by design; plain Lodash is data-first and suits chaining.

Exercises

curry, partial, pipe and compose

Implement an auto-currying curry(fn) (calls fn once fn.length arguments have arrived, accepting any grouping). With the numbers on line 1 print curry=<add3(a)(b)(c)>,<add3(a, b)(c)>,<add3(a)(b, c)>,<add3(a, b, c)>; then pipe(inc,double)(a)=<> compose(inc,double)(a)=<>; then partial=<partial(greet, "Hello", "!")("Ada")> | <partial(greet, "Hi")("?", "Bob")> for greet(greeting, punctuation, name) → <greeting>, <name><punctuation>; then slug=<slugify(line)> for each remaining line via a pipe of trim+lower-case, non-alphanumerics → -, strip edge dashes; finally arityTrap: length=<((x, y = 0) => x + y).length> curried(5)=<what curry gives for that function called with 5>.

Example: 1 2 3 / Hello, World! →

curry=6,6,6,6
pipe(inc,double)(1)=4 compose(inc,double)(1)=3
partial=Hello, Ada! | Hi, Bob?
slug=hello-world
arityTrap: length=1 curried(5)=5

A default parameter does not count toward length, so the curried function fires after one argument.

A data-last pipeline over records

Write curried, data-last helpers map(f, xs), filter(p, xs), sortBy(key, xs) (a sorted copy), take(k, xs), prop(k) and countBy(f, xs) (a Map). Line 1 is n; the rest are name age city. Build youngestAdults = pipe(filter(age ≥ 18), sortBy("age"), map(prop("name")), take(n)) and print youngestAdults=<JSON>; then byCity=<city=count sorted by city> via countBy(prop("city")); then over30=<names with age > 30 via a pipeline built by a function namesOver(age)> reusedOnSubset=<youngestAdults applied to the first two records>.

Example: 2 then ada 36 london, bob 17 paris, cy 22 london, dee 41 rome, eve 19 paris →

youngestAdults=["eve","cy"]
byCity=london=2 paris=2 rome=1
over30=["ada","dee"] reusedOnSubset=["ada"]

The same composed function ran on two different arrays — that is what data-last buys.

In this module: Functional patterns

← Pure functions and immutability — why, and how far to take it · Folds, recursion and trampolines — reduce as the universal loop →