Functional Intuition
Why "pure functions", "immutability", and "data in, data out" lead to programs that are easier to think about.
Now that you've met map, filter, and reduce, you've actually
been doing a kind of programming called functional
programming (FP) — even if no one used the phrase. This chapter
makes the underlying ideas explicit. You don't need to become a
strict functional programmer to benefit from them; you just need
the intuition.
Three ideas do most of the work:
- Pure functions — same input, same output, no surprises.
- Immutability — prefer creating new values to mutating old ones.
- Data in, data out — programs as pipelines of transformations.
Pure functions
A pure function is one that:
- Returns the same output every time it's called with the same input.
- Does not change anything outside itself (no side effects).
Pure functions are easy to reason about because you only need to look at their arguments to predict their output. No hidden state, no surprises.
"Same input, same output" sounds boring. That's the point — boring is exactly the property you want when debugging.
Side effects are not evil
Real programs must eventually do I/O — print things, save files, send network requests, update the screen. A program of only pure functions would never affect the outside world.
The functional advice is not "never have side effects" — it's push them to the edges of your program and keep the core logic pure.
Imperative shell, functional core. Most modern code follows this shape, whether or not the author names it.
Immutability
A value is immutable when, once created, it cannot change.
JavaScript's primitives (numbers, strings, booleans) are already
immutable — "hello".toUpperCase() returns a new string,
"hello" itself never changes.
Objects and arrays are mutable by default. Functional code treats them as if they weren't.
Why prefer the immutable style?
- No spooky action at a distance. If you pass an array to a function and trust it not to mutate, the caller's data stays safe.
- Easier to compare. "Did this change?" is just
oldValue !== newValue. - Easier to undo / time-travel. Old versions are still around.
- Works nicely with concurrency (relevant in async code).
The trade-off: you allocate more objects. For typical app code, this cost is negligible.
Mutating methods vs. non-mutating
A handful of array methods mutate; most don't. It's worth memorising the mutators because they're the ones that bite you.
| Mutating | Non-mutating (returns new) |
|---|---|
push, pop, shift, unshift | concat, [...arr, x] |
splice | slice, toSpliced |
sort, reverse | toSorted, toReversed |
arr[i] = x | arr.map((v, i) => i === idx ? x : v) |
When you can, prefer the non-mutating version.
Functions as values
We already saw this in the functions chapter, but it's worth repeating because it's the bedrock of FP: in JavaScript, functions are values like any other. You can:
- Assign them to variables.
- Put them in arrays.
- Pass them to other functions.
- Return them from functions.
Treating functions as data is what enables map/filter/reduce,
event handlers, callbacks, middleware, promise chains, and dozens
of other patterns you'll keep meeting.
Composition
A pipeline like arr.filter(...).map(...).reduce(...) is a form
of composition: small functions stuck together to make a
bigger one. You can compose at the function level too.
pipe is a tiny but powerful tool: it expresses "do these things
in order" as data. Many real-world libraries (Ramda, Lodash/fp,
RxJS) make this style the default.
Declarative vs. imperative
A useful pair of words to know:
- Imperative code says how to do something, step by step.
- Declarative code says what the result should be.
map, filter, and reduce are declarative. SQL is declarative.
JSX is declarative. A for loop is imperative.
Both styles are valid. Use whichever makes the code clearer for its purpose. Declarative tends to win when describing transformations of data; imperative often wins for low-level algorithms or performance-critical inner loops.
A practical refactor
Let's take some imperative code and gradually make it more functional. None of these versions is "wrong" — they're trade-offs.
All three return 380. The third version reads like the
specification: "of the paid orders, sum the amounts."
Multi-file: keeping the core pure
The summary.js module is entirely pure. You could test it
without ever printing anything: feed it orders, inspect the
returned object. The index.js module does the impure part —
prints to the terminal.
Challenge
Write a function incrementScores(students, name) that, given an array of { name, score } objects, returns a new array where the matching student's score is incremented by 1.
Rules:
- Do NOT mutate the original array.
- Do NOT mutate any element object.
- If no student matches, return an equivalent new array.
This is a classic immutable update.
A function is called "pure" when it...
always returns the same type
uses arrow-function syntax
returns the same output for the same input and has no side effects
doesn't use any if statements