Command Palette

Search for a command to run...

Hectal
PHASE 15Advanced ~28 min· topic 1 of 8

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: pipe runs them left to right, compose right to left.
Currying
Turning f(a, b, c) into f(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.

bug.jswhole filejavascript
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".

cart.jswhole filejavascript
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.

pipe.jswhole filejavascript
const pipe = (...fns) => (x) => fns.reduce((acc, f) => f(acc), x);
const toSlug = pipe(trim, lower, dashes);
Build a pipelinediagram
Rendering diagram…

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)).

curry.jswhole filejavascript
const multiply = (a) => (b) => a * b;
const double = multiply(2);   // remembers a = 2
double(21);                   // 42

05Write 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. 1

    Add a step to the pipeline

    In the second example, write const exclaim = (s) => s + "!"; and add it at the end of toSlug's pipe. Predict the first line, then run it. What happens if you put it first instead?

  2. 2

    Feel the shallow freeze

    In the first example, wrap the inner array too: sizes: Object.freeze([1, 2]). Predict: does settings.sizes.push(3) still work? Run it and read the error.

  3. 3

    Break the curry helper

    Give volume a default parameter: (l, w, h = 1) => l * w * h. fn.length now counts only parameters before the first default. Predict what volume(2) returns, then check.

Code & diagrams

Immutability: copy, don't change New tab

`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

Press Run to see the page.

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
pipe, compose, currying and partial application ES2015+ New tab

`hello` is `greet` with its first argument fixed, so it drops straight into `map`. `volume` accepts its arguments in any grouping.

Preview

Press Run to see the page.

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!' ]
Updating nested state without mutation New tab

Unchanged parts (`todos`, the first todo) are **shared** between old and new state. That sharing is what makes immutable updates cheap.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Delhi Pune
true
true false true
[ 1, 20, 3 ]
Delhi Goa

Break 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(...).

terminal
$ Console
── what you'll see ──
[ 10, 20, 30 ] [ 10, 20, 30 ]

Break #2

Freeze in strict mode

Add "use strict"; as the very first line of the first example.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot assign to read only property 'theme' of object '#<Object>'

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 only toSorted, toReversed and with exist.

  • ▸

    structuredClone (an HTML-standard API, also in Node 17+) copies Map, Set, Date, typed arrays and circular references, but throws a DataCloneError on functions and DOM nodes, and drops class prototypes (a cloned class instance becomes a plain object).

  • ▸

    fn.length ignores rest parameters and anything from the first default value on, so generic curry helpers are fragile with such functions. Libraries like Ramda accept an explicit arity: curryN(3, fn).

  • ▸

    Deep pipe chains 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. 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 + b is 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. 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 methods toSorted, toReversed, toSpliced and with(index, value). A big win: comparing oldState === newState instantly tells you whether anything changed, which is how React and many state libraries decide what to redraw.

  3. 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 with structuredClone(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 a TypeError.

  4. 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) means h(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-line reduce (Phase 5). Composition only works smoothly when each function takes one input and returns one output, which is where currying helps.

  5. 5

    Currying turns a function of several arguments into a chain of one-argument functions: add(a, b, c) becomes add(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 general curry(fn) helper uses fn.length (the number of declared parameters) to know when it has collected enough arguments and can call the real function.

  6. 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 arrow name => 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 into map, filter or a pipe.

Explain it without notes

01

What is a pure function, and why does it matter?

02

How do you update a nested property immutably, and why is spread called shallow?

03

What is the difference between currying and partial application?

04

Implement pipe and explain how it differs from compose.

Practice

01

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.

02

Write a curried filterBy(key)(value)(list) and use it to get the books by "Ruskin" from a list.

03

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.freeze are shallow, and when to use structuredClone.

  • I can write pipe, compose and curry from memory.

  • I can use partial application to make a specialised function for map or filter.