Function Composition
Plumbing small pure functions into pipelines — the operational heart of declarative code, plus currying and partial application.
If pure functions are the atoms of FP, composition is the chemistry: the operation that takes two (or many) functions and glues them into one. Once your functions are pure, you can build entire programs by composing them — no shared state, no intermediate variables, no plumbing.
This chapter covers function composition, the closely related
techniques of currying and partial application, and the two
helper functions (compose and pipe) you'll use everywhere.
What composition means, mathematically
In math, given f: A → B and g: B → C, the composition g ∘ f
is the function A → C defined by (g ∘ f)(x) = g(f(x)). You apply
f first, then feed the result into g.
The two diagrams above describe the same function. Composition is
the operation that lets us treat "do f then g" as a single
named thing.
Composition in TypeScript
We have to write it ourselves. Here's the simplest possible binary composer:
Two things to notice:
- The argument order in
compose2(g, f)reads "right to left":g(f(x)). This is the mathematical convention. - Composition is not commutative:
compose2(g, f)is different fromcompose2(f, g). Order matters.
pipe: the left-to-right cousin
In practice, most TypeScript code reads better when the data flows
forward: input on the left, transformations on the right. That's
what pipe does:
pipe(value, f, g, h) evaluates as h(g(f(value))). It is exactly
compose reversed. We will use pipe throughout the course because
it reads top-to-bottom, like the data flow it represents.
Variadic compose
A compose that takes any number of functions is the symmetric
counterpart:
For most working code, pipe is more readable; compose shows up
when you want to name a composed function for reuse:
normalize is a value. You can pass it to Array#map, store it,
re-export it, test it. It is just itself — three small functions
glued together.
Pipelines vs nested calls: the readability test
Before pipelines:
With named composed steps:
Both are correct. The second one is legible: each step has a name, and the pipeline reads like a recipe.
Currying: one argument at a time
Currying is the transformation that turns a function of multiple arguments into a chain of one-argument functions.
A curried function of N arguments has type A1 => A2 => ... => AN => R. Each application "consumes" one argument and returns a smaller
function.
Currying is useful primarily because it makes partial application trivial: you supply some arguments now and the rest later.
Partial application
You can partially apply any function — curried or not — by binding some arguments and leaving others as parameters.
A partially applied function is just an ordinary function whose arguments have been supplied incrementally. It has the same type, same behavior, and same testability as any other function.
Why argument order matters
When you curry a function for use in pipelines, the convention is "data last": the value you're transforming should be the last argument, so partial application gives you a "ready-to-pipe" function.
Compare with "data-first" (map(xs, f)), which doesn't compose into
a pipeline without an extra wrapper. The whole point of currying in
FP is to make pipe(value, step, step, step) work without ceremony.
A real refactor: from imperative to composed pipeline
Same problem we used in Declarative Thinking: "From orders, keep paid > 50, distinct customers, sorted alphabetically."
Each step has a name and a single job. You can read the pipeline as
prose. You can lift any step into a separate file. You can test any
step in isolation. And the order of the steps is just the order of
arguments to pipe — no reshuffling of loops.
A multi-file challenge
In text.ts, implement five tiny pure helpers and a
pipe of them. Each helper does exactly one thing.
trim(s)— strip surrounding whitespacelower(s)— lowercasestripPunct(s)— remove non-alphanumeric, non-space characterscollapseSpaces(s)— collapse runs of whitespace to onehyphenate(s)— replace spaces with hyphensslug(s)— pipeline of the above, in this order
Use pipe (already in the file). Do not write s.trim().toLowerCase()… — compose the named helpers explicitly with pipe.
Expected output
hello-world
functional-programming
nice-to-meet-you
What is the difference between compose(f, g) and pipe(x, g, f)?
compose is for synchronous functions and pipe is for asynchronous functions
compose curries its arguments; pipe does not
They express the same idea in different argument orders; compose(f, g)(x) = f(g(x)), while pipe(x, g, f) applies g first then f
compose can only combine two functions; pipe works for any number