This site starts you on functional programming with Refactoring: rewrite an imperative loop as map, filter, and reduce. If you have never looked at FP before, that first step can feel like a style preference with no reason behind it. It is not. Functional programming is a small set of rules, and the exercises on this site exist to make those rules automatic. This article is the “why”: the rules themselves, in JavaScript, for someone who has been writing JS without ever thinking about them.
A function is a mapping, not a procedure
In imperative JavaScript, a function is a list of things to do: read this variable, mutate that array, log the result. In functional programming, a function is a mapping from inputs to outputs, the same thing as a mathematical function. You hand it an argument; it hands back a value. Nothing else happens.
// imperative: a procedure
function doubleAll(xs) {
for (let i = 0; i < xs.length; i++) {
xs[i] = xs[i] * 2; // mutates the caller's array
}
return xs;
}
// functional: a mapping
function doubleAll(xs) {
return xs.map((x) => x * 2); // returns a new array
}
The difference is not the loop. It is what the function is allowed to do. The imperative version reaches out and changes a value the caller still holds. The functional version only returns a value. The two rules below follow from treating a function as a mapping and nothing more.
Rule 1: referential transparency
An expression is referentially transparent if you can replace it with the value it produces and the program behaves the same. 2 + 3 is transparent: wherever you see it, you can write 5 instead and nothing changes.
A function call is transparent when calling it has no effect beyond the value it returns. Same input, same output, every time. No hidden state, no clock, no random number, no console.log, no writing to a variable the caller can see. This is what “pure function” means: the result depends only on the arguments.
The opposite is a side effect: anything a function does besides return a value. Mutating a shared object is a side effect. Reading Date.now() is a side effect. So is a network call. Side effects are not evil; every useful program has some. The rule is not “never have side effects.” The rule is know where they are, and push them to the edge of the program, away from the code that reasons about data.
Why does this rule matter? A transparent function is a black box you can understand in isolation. You do not need to know what the rest of the program was doing when it was called. Given the same input, it gives the same answer, so you can test it, memoize it, reorder it, run it in parallel, and reason about it without holding the whole program in your head.
Rule 2: immutability
A value is immutable when it never changes after it is created. Numbers and strings in JS already work this way: "abc".toUpperCase() returns a new string; it does not rewrite the old one in place. Arrays and objects are where JS lets you cheat.
const scores = [3, 5, 1];
scores.sort(); // mutates scores in place — the old value is gone
The functional version creates a new value instead:
const scores = [3, 5, 1];
const sorted = [...scores].sort((a, b) => a - b);
// scores is still [3, 5, 1]; sorted is [1, 3, 5]
The difference is subtle in a ten-line example and enormous in a real program. When data is immutable, the value you are looking at right now is the value it will always be. No other part of the program can change it out from under you between the line where you read it and the line where you use it. When data can be mutated, you have to remember who else holds a reference and when they might write to it. That is where most subtle bugs live.
Notice the two rules reinforce each other. A function that mutates its input is not referentially transparent: call it twice and the second call behaves differently, because the input changed. Pure functions and immutable data are the same idea from two directions. A function may only compute a new value, never modify an old one.
The payoff: functions become values
Once a function is a pure mapping, it is safe to treat it as a value. Pass it to another function, return it from a function, store it in a variable. This is what “first-class functions” means, and it is the reason FP’s standard tools exist.
map, filter, and reduce are loops with the loop factored out and the per-element action passed in:
xs.map((x) => x * 2); // transform each element
xs.filter((x) => x > 0); // keep matching elements
xs.reduce((acc, x) => acc + x, 0); // fold the list into one value
You have probably used all three. The FP idea is to use them instead of writing the loop by hand. A hand-written loop mixes two concerns: how to walk the list, and what to do with each element. The “what to do” part is almost always a pure function you could name and test on its own.
Where this site takes it
This article is the theory. The site turns it into practice:
-
Refactoring makes the two rules mechanical: rewrite an imperative loop as
map/filter/reduce, and feel what it is like when a function stops mutating and starts returning. - Tracing builds the substitution model: the habit of replacing a call with its value, one step at a time. That is referential transparency practiced by hand.
- Substitutions and Functions drill the same idea at the expression level: two expressions are equivalent if swapping one for the other never changes the result.
The two rules are the whole contract. Everything else in functional programming (higher-order functions, composition, even the lambda calculus at the end of this site’s path) is a consequence of a function being a mapping, nothing more, that you may always replace with its value.