Topic 15.1
Functional Patterns
In one line
Functional patterns build programs from small functions that don't change shared data: immutability keeps values safe, composition chains functions into pipelines, and currying and partial application make specialised functions from general ones.
Think of it like this
A sandwich assembly line. Each worker does one small job: one cuts the bread, the next adds cheese, the next wraps it. No worker reaches back and changes what an earlier worker did; they take what arrives and pass on something new. To make a different sandwich you rearrange the workers instead of retraining them. Composition is the line, each pure function is a worker, and immutability is the rule "never change what you were handed".
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Pure function
- A function whose answer depends only on its inputs and which changes nothing outside itself.
- Side effect
- Anything a function does besides returning a value, like changing a global variable, the page or storage.
- Immutability
- Never changing a value after it is made; you build a new value with the change instead.
- Shallow copy
- A copy of the outer object or array only; objects inside it are still shared with the original.
- Composition
- Joining small functions so the output of one is the input of the next.
- pipe / compose
- Helpers that compose functions:
piperuns them left to right,composeright to left. - Currying
- Turning
f(a, b, c)intof(a)(b)(c): one argument per call. - Partial application
- Fixing some of a function's arguments now, getting back a function that takes the rest.
- Closure
- A function together with the variables it remembers from where it was created.
Step by step
01Spot the shared-data bug
Two parts of a program hold the same cart object. One part "adds an item" with push. The other part is surprised: its cart changed too, because both variables point to the same object in memory.
const cart = { items: ["pen"] };
const saved = cart; // not a copy: same object
cart.items.push("lamp");
console.log(saved.items); // [ 'pen', 'lamp' ]02Return a new value instead
The immutable version builds a new object with spread, and a new array inside it. The old cart is untouched, so anyone holding it still sees what they had. oldCart !== newCart now reliably means "something changed".
const addItem = (cart, item) => ({ ...cart, items: [...cart.items, item] });03Build a pipeline
pipe takes a list of functions and returns a new function. When you call it with a value, reduce feeds the value through each function in turn.
const pipe = (...fns) => (x) => fns.reduce((acc, f) => f(acc), x);
const toSlug = pipe(trim, lower, dashes);04Curry to make one-argument steps
A pipeline step must take one value. multiply(a, b) takes two, so it doesn't fit. Curried, multiply(2) is a one-argument function "double it" that fits anywhere: pipe(multiply(2), add(1)).
const multiply = (a) => (b) => a * b;
const double = multiply(2); // remembers a = 2
double(21); // 4205Write a general curry helper
curry(fn) collects arguments across calls. When it has at least fn.length of them, it calls fn; otherwise it returns a function waiting for more. Each waiting function is a closure holding the arguments so far.
06Keep side effects at the edges
The final shape of a functional program: an event arrives (edge), pure functions compute the new state (middle), and one render function updates the page (edge). The capstone in Topic 15.8 uses exactly this shape.
Try it yourself
- 1
Add a step to the pipeline
In the second example, write
const exclaim = (s) => s + "!";and add it at the end oftoSlug'spipe. Predict the first line, then run it. What happens if you put it first instead? - 2
Feel the shallow freeze
In the first example, wrap the inner array too:
sizes: Object.freeze([1, 2]). Predict: doessettings.sizes.push(3)still work? Run it and read the error. - 3
Break the curry helper
Give
volumea default parameter:(l, w, h = 1) => l * w * h.fn.lengthnow counts only parameters before the first default. Predict whatvolume(2)returns, then check.
Code & diagrams
`Object.freeze` blocked the change to `theme`, but the array inside was not frozen. The last line shows the mutating version handing back the very same object.
Preview
Console
Nothing logged yet.Expected console output
[ 'pen', 'book' ]
[ 'pen', 'book', 'lamp' ]
false false
[ 30, 10, 20 ] [ 10, 20, 30 ]
{ theme: 'dark', sizes: [ 1, 2, 3 ] }
[ 'pen', 'book', 'cup' ] true`hello` is `greet` with its first argument fixed, so it drops straight into `map`. `volume` accepts its arguments in any grouping.
Preview
Console
Nothing logged yet.Expected console output
hello-big-world
same-result
6
16
24 24 24
Hello, Ravi! Hi, Mei!
[ 'Hello, Ana!', 'Hello, Bo!' ]Unchanged parts (`todos`, the first todo) are **shared** between old and new state. That sharing is what makes immutable updates cheap.
Preview
Console
Nothing logged yet.Expected console output
Delhi Pune
true
true false true
[ 1, 20, 3 ]
Delhi GoaBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Sort the shared array in place
In the first example, replace scores.toSorted(...) with scores.sort(...).
Break #2
Freeze in strict mode
Add "use strict"; as the very first line of the first example.
Myth vs fact
Myth
const makes an object immutable.
Fact
const only stops the variable from being pointed at something else. The object it points to can still be changed: const a = []; a.push(1) works.
Myth
Immutable code is slow because it copies everything.
Fact
Spread copies only the level you change; everything else is shared by reference. For most UI state this costs almost nothing, and it makes "did it change?" a single === check.
Myth
Functional programming means no side effects at all.
Fact
A program with no side effects can't show anything. The goal is to isolate side effects at the edges so the core logic stays pure and testable.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The ES2023 "change array by copy" methods (
toSorted,toReversed,toSpliced,with) exist exactly to support immutable updates without spreading first. On typed arrays onlytoSorted,toReversedandwithexist. - ▸
structuredClone(an HTML-standard API, also in Node 17+) copiesMap,Set,Date, typed arrays and circular references, but throws aDataCloneErroron functions and DOM nodes, and drops class prototypes (a cloned class instance becomes a plain object). - ▸
fn.lengthignores rest parameters and anything from the first default value on, so genericcurryhelpers are fragile with such functions. Libraries like Ramda accept an explicit arity:curryN(3, fn). - ▸
Deep
pipechains create one closure per step and are hard to step through in a debugger. A TC39 proposal for a pipe operator (|>) has sat at stage 2 for years; don't rely on it.
Remember this
- 1
A pure function gives the same output for the same input and has no side effects (a side effect is anything a function does besides returning a value: changing a variable outside itself, writing to the page, logging, saving to storage).
(a, b) => a + bis pure. A function that pushes into a global array is not. Pure functions are easy to test (call it, check the answer), easy to cache (Topic 15.3) and safe to run in any order. Real programs need side effects, so the pattern is to keep them at the edges (event handlers, the render step) and keep the logic in the middle pure. - 2
Immutability means not changing a value after it's made; you make a new value instead. Strings and numbers are already immutable. Objects and arrays are not:
arr.push(x)changes the array that every variable pointing to it sees (Phase 6 showed that objects are shared by reference). The immutable way is to copy with changes:{ ...obj, done: true },[...arr, item],arr.filter(...),arr.map(...), and the ES2023 copy methodstoSorted,toReversed,toSplicedandwith(index, value). A big win: comparingoldState === newStateinstantly tells you whether anything changed, which is how React and many state libraries decide what to redraw. - 3
Spread copies are shallow:
{ ...user }makes a new outer object but nested objects are still shared. To change something deep, copy each level on the path ({ ...user, address: { ...user.address, city: "Pune" } }), or make a full deep copy withstructuredClone(value)(a browser and Node function, not part of ECMAScript itself).Object.freeze(obj)stops changes to an object's own properties; it is also shallow, and in sloppy (non-strict) code a blocked write fails silently, while in strict mode and modules it throws aTypeError. - 4
Composition joins small functions into a bigger one, where the output of one becomes the input of the next.
pipe(f, g, h)(x)meansh(g(f(x))): read left to right, like the assembly line.compose(f, g, h)(x)is the same idea read right to left,f(g(h(x))), as in maths. Both are a one-linereduce(Phase 5). Composition only works smoothly when each function takes one input and returns one output, which is where currying helps. - 5
Currying turns a function of several arguments into a chain of one-argument functions:
add(a, b, c)becomesadd(a)(b)(c). Each call returns a new function that remembers the arguments so far through a closure (Phase 3: a function keeps access to the variables of the place it was created). A generalcurry(fn)helper usesfn.length(the number of declared parameters) to know when it has collected enough arguments and can call the real function. - 6
Partial application fixes some arguments now and leaves the rest for later, giving a more specific function:
const hello = greet.bind(null, "Hello"), or an arrowname => greet("Hi", name). Currying is one way to get it, but partial application doesn't need one-argument-at-a-time calls. Put the argument that changes least first (the config, the rate, the greeting) and the data last, so the partly applied function slots straight intomap,filteror apipe.
Explain it without notes
What is a pure function, and why does it matter?
How do you update a nested property immutably, and why is spread called shallow?
What is the difference between currying and partial application?
Implement pipe and explain how it differs from compose.
Practice
Write updateTodo(todos, id, changes) that returns a new array where the todo with that id is merged with changes, without changing the original. Test it.
Write a curried filterBy(key)(value)(list) and use it to get the books by "Ruskin" from a list.
Use pipe to build total that takes a cart array of { price, qty }, multiplies each line, adds them up, and adds 18% tax rounded to 2 decimals.
Trade-offs
- ↔
Immutable updates make change detection and undo trivial, but deep nested updates get verbose; flattening state (storing items by id) or a helper library (such as Immer) keeps them readable.
- ↔
Point-free pipelines of tiny functions read beautifully when they're short, but long chains of curried helpers can be harder for a team to read and debug than a plain loop.
Done when you can
I can tell a pure function from one with side effects.
I can update nested objects and arrays without mutating them.
I know spread and
Object.freezeare shallow, and when to usestructuredClone.I can write
pipe,composeandcurryfrom memory.I can use partial application to make a specialised function for
maporfilter.