22 · Functional Programming Foundations
The functional paradigm from first principles: pure functions, immutability, first-class and higher-order functions, composition, currying and partial application, recursion, and then the category-theory-flavored abstractions every senior eventually meets — functors, applicatives, and monads (Maybe, Either, IO). Plus why React is “functional-ish” and where it isn’t.
Positioning
Functional programming (FP) is the paradigm most of modern frontend has quietly adopted: React is built on pure-ish functions of props → UI, hooks favor immutability, Redux is reducers (pure functions) over immutable state, array methods (map/filter/reduce) are FP, and async/Promises are monadic. You don’t need Haskell to be senior, but you must understand purity, immutability, composition, and at least conceptually what a functor/monad is — because those words appear in libraries, docs, and interviews, and the ideas make your code more predictable and testable. This file assumes only basic programming knowledge and builds up; it pairs with OOP (21) — real systems blend both.
Foundations: the core idea
FP models computation as the evaluation of functions that transform input to output, avoiding shared mutable state and side effects. The mental shift from imperative code (you know: loops mutating variables step by step) is: describe what to compute as data transformations, not how to step through it.
// Imperative: mutate as you go
let total = 0;
for (let i = 0; i < cart.length; i++) total += cart[i].price;
// Functional: declarative transformation
const total = cart.reduce((sum, item) => sum + item.price, 0);
The payoff: code that’s easier to reason about, test, parallelize, and compose, because functions are predictable and don’t secretly affect the rest of the program.
Deep dive: the building blocks
1. Pure functions
A function is pure if (1) its output depends only on its inputs (same input → same output, “referential transparency”), and (2) it has no side effects (doesn’t mutate external state, do I/O, log, or touch the network/DOM).
const add = (a, b) => a + b; // pure
let count = 0; const inc = () => ++count; // impure: mutates external state
Purity is the foundation everything else rests on: pure functions are trivially testable (no mocks/setup), cacheable (memoizable, 05), reorderable, and impossible to break from a distance. You can’t eliminate side effects (a program with no I/O is useless) — the FP strategy is to push them to the edges and keep a large pure core (this mirrors hexagonal architecture, 10).
2. Immutability
Don’t mutate data; produce new values instead. This kills a whole class of bugs from shared mutable references (recall pass-by-reference: two variables pointing at the same object means a mutation through one is visible through the other — aliasing bugs).
// ❌ mutation: callers sharing this array see the change
const addItem = (cart, item) => { cart.push(item); return cart; };
// ✅ new array, original untouched
const addItem = (cart, item) => [...cart, item];
const updated = { ...user, name: 'Ann' }; // new object
- In JS, use spread/
map/filter,Object.freeze(shallow), or libraries like Immer (write “mutating” code, get an immutable copy via Proxy) and Immutable.js. React relies on immutability for change detection (05,06) — mutating state in place is a classic bug. - Immutability + purity = predictable state transitions (Redux reducers,
06).
3. First-class & higher-order functions
First-class: functions are values — assign them, pass them, return them. Higher-order function (HOF): a function that takes and/or returns a function. JS has had this forever; it’s why map/filter/reduce, event handlers, and hooks work.
const withLogging = (fn) => (...args) => { console.log(args); return fn(...args); }; // HOF
[1,2,3].map(x => x * 2); // map takes a function
map, filter, reduce are the FP workhorses: reduce is the universal one — map and filter can both be expressed as reduces. Master them and you rarely need raw loops.
4. Function composition
Combine small functions into bigger ones: the output of one becomes the input of the next. compose(f, g)(x) = f(g(x)); pipe is the left-to-right version.
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const slugify = pipe(s => s.trim(), s => s.toLowerCase(), s => s.replace(/\s+/g, '-'));
slugify(' Hello World '); // "hello-world"
Composition is the FP analog of building with small parts (composition-over-inheritance, 20/21). Point-free style writes functions as compositions without naming the argument — elegant but can hurt readability; use judiciously.
5. Currying & partial application
Currying: transform a multi-arg function into a chain of single-arg functions. Partial application: fix some arguments now, supply the rest later. Both enable reuse and composition.
const add = (a) => (b) => a + b; // curried
const add10 = add(10); add10(5); // 15 (partial application)
const multiply = (a, b) => a * b;
const double = multiply.bind(null, 2); // partial application via bind
Useful for building specialized functions and for making functions composable (single-argument functions compose cleanly).
6. Recursion
FP often replaces loops with recursion (a function calling itself toward a base case). Natural for tree/nested structures (the DOM, JSON, file systems, 23). Watch the call-stack limit in JS — deep recursion can overflow (JS engines don’t reliably do tail-call optimization), so iterative or trampolined versions are sometimes needed for large inputs.
The category-theory layer: functors, applicatives, monads
These sound scary; the ideas are simple — they’re patterns for working with values inside a “container/context” (a box that might be empty, async, or error-carrying) without manually unwrapping it everywhere. You already use them.
Functor — “mappable”
A functor is any container with a map that applies a function to the value(s) inside, preserving the container’s structure. The law: map(id) === id and map(f∘g) === map(f)∘map(g).
- Array is a functor:
[1,2].map(f)appliesfinside the array context. - A
Maybe/Option(a box that’sJust(x)orNothing) is a functor:maprunsfif there’s a value, skips if empty — no null checks scattered everywhere.
const Just = (x) => ({ map: (f) => Just(f(x)), value: x });
const Nothing = () => ({ map: () => Nothing(), value: null });
const safeUpper = (m) => m.map(s => s.toUpperCase());
safeUpper(Just('hi')); // Just('HI')
safeUpper(Nothing()); // Nothing() — the function just didn't run; no crash
Applicative — apply a wrapped function to wrapped values
Lets you apply a function that’s inside a context to arguments that are also inside contexts (e.g., combine several Maybes, validate multiple fields accumulating errors). One step up from functor.
Monad — “chainable” (flatMap)
A monad is a functor that also has flatMap (a.k.a. chain/bind) and a way to wrap a value (of/return). It lets you sequence operations that each return a wrapped value, without nesting boxes-in-boxes. The famous (tongue-in-cheek) definition: “a monad is a monoid in the category of endofunctors” — ignore that; the practical meaning is “a composable way to chain context-producing steps.”
- Promise is essentially a monad:
.thenismap+flatMapcombined — chain async steps without nesting callbacks (callback hell was the absence of this).async/awaitis monadic sequencing with nicer syntax. Maybeas a monad: chain operations that each might fail, short-circuiting on the firstNothing— no nested null checks.Either/Result(Left(error)|Right(value)): chain operations that might error, carrying the error along — typed error handling without exceptions. Common in robust validation/parsing (Zod-style,03/12).IO/Task: wrap side effects as values so the pure core stays pure and effects run at the edge.
// Either: railway-oriented programming — each step continues on Right, short-circuits on Left
parseInput(raw) // Right(data) | Left(error)
.flatMap(validate) // only runs if previous was Right
.flatMap(save)
.fold(onError, onOk); // collapse the box at the very end
You don’t need a library to benefit — but fp-ts, Effect, and Ramda package these rigorously for TypeScript. In day-to-day React you mostly meet monads as Promises and as the idea behind clean error/optional handling.
Other FP vocabulary worth knowing
- Monoid — a type with an associative combine operation and an identity (numbers under
+/0, arrays under concat/[], strings under concat/''). Underliesreduceand parallel aggregation. - Algebraic Data Types (ADTs) — sum types (discriminated unions,
03) + product types (records); model data precisely so illegal states are unrepresentable (20). - Lenses — composable getters/setters for immutable nested updates.
FP in React (and where React is impure)
React leans functional: a component is conceptually props → UI (pure render), reducers are pure, derived state is computed not stored, and immutability drives change detection.
- Pure render: don’t mutate props/state or do side effects during render. Side effects go in
useEffect/event handlers — React’s way of pushing effects to the edge (05). useReducer/Redux are textbook FP:(state, action) => newState, pure and immutable (06).mapto render lists, derived values over stored duplicates (SSOT,20).- React is not pure overall — hooks have order/identity, effects touch the world, refs are mutable escape hatches — but the programming model it rewards is functional. The React Compiler (
05) exploits this purity to auto-memoize.
Worked example: pure core + effects at the edge (Maybe + pipe)
// Pure transformations (testable with zero setup)
const parsePrice = (s: string) => (Number.isFinite(+s) ? Just(+s) : Nothing());
const applyTax = (rate: number) => (n: number) => n * (1 + rate);
const format = (n: number) => new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(n);
function priceLabel(raw: string): string {
return parsePrice(raw) // Maybe<number> — no manual NaN checks downstream
.map(applyTax(0.1)) // functor: runs only if valid
.map(format)
.value ?? '—'; // collapse the box once, at the boundary
}
// The IMPURE edge stays thin and separate:
button.addEventListener('click', () => { label.textContent = priceLabel(input.value); });
The math/formatting is pure and trivially unit-tested; the DOM side effect lives at the edge. Same architecture as a clean/hexagonal app (10), expressed in the small.
Pitfalls & gotchas
- Accidental mutation (especially of state/props) → React doesn’t re-render or shows stale data; aliasing bugs from shared references.
- Hidden side effects in “pure” functions → breaks testability and reasoning; keep effects at the edge.
- Over-currying / aggressive point-free → unreadable cleverness (KISS,
20). - Deep recursion in JS → stack overflow (no reliable TCO); use iteration for large inputs.
forEachwith mutation instead ofmap/reducereturning new data.- Monad-washing → wrapping everything in
Either/IOin a codebase that doesn’t need it; adopt FP abstractions where they reduce real pain, not for purity points. - Confusing immutability with deep-freeze → spread is shallow; nested objects still share references (use Immer or structured updates).
- Reinventing array methods with loops → prefer
map/filter/reduce.
Interview questions
- What makes a function pure, and why does it matter?
- Why is immutability important in React/Redux specifically?
- What’s a higher-order function? Give three you use daily.
- Implement
pipe/compose. What problem do they solve? - Currying vs partial application — difference and a use case.
- What is a functor? Why is Array one? Why is
Maybeuseful? - What is a monad, practically — and why is
Promisemonad-like? - How does
Either/Resultimprove error handling vs exceptions? - Where should side effects live in a functional architecture?
- Where is React functional, and where is it deliberately not?
Recommendations
- Default to pure functions + immutability; push side effects to the edges (effects, handlers, gateways).
- Master
map/filter/reduceandpipe/compose; reach for raw loops rarely. - Use immutable updates (spread/Immer); never mutate React state/props.
- Model optional/error flows with discriminated unions /
Result(03) to make illegal states unrepresentable (20). - Understand functor/monad as patterns (mappable/chainable contexts); adopt fp-ts/Effect only where the rigor pays off.
- Blend paradigms: OO boundaries/objects where they fit (
21), functional cores everywhere.
Books & references
- “Professor Frisby’s Mostly Adequate Guide to Functional Programming” — Brian Lonsdale (free online). The best gentle path in JavaScript from pure functions through functors and monads. Start here.
- “Functional-Light JavaScript” — Kyle Simpson (free on GitHub). Pragmatic FP for JS without the jargon overload.
- “Grokking Simplicity” — Eric Normand. The clearest book on actions vs calculations vs data and pushing effects to the edge; ideal from-zero.
- “Functional Programming in JavaScript” — Luis Atencio.
- fp-ts / Effect docs (effect.website) — rigorous functors/monads/Either in TypeScript, production-grade.
- Ramda docs (ramdajs.com) — curried, composable utility functions to learn the style.
- “Structure and Interpretation of Computer Programs” (SICP) — Abelson & Sussman (free) — the deep, classic foundation (Scheme, but timeless).
- “Category Theory for Programmers” — Bartosz Milewski (free) — only if you want the real theory behind functors/monads.
Connections
21-oop-foundations.md— the complementary paradigm; senior code blends OO structure with FP cores.20-programming-principles.md— purity/immutability/composition are these principles in functional form.05-react-internals-and-patterns.md— pure render, reducers, immutability-driven reconciliation, the Compiler.06-state-management-and-stores.md— reducers, immutable state, derived/selectors as FP.03-typescript.md— discriminated unions/ADTs, generics, making illegal states unrepresentable.02-javascript-deep-dive.md— closures (the engine of HOFs/currying), array methods, Promises as monads.10-frontend-architecture.md— pure core + effects at the edge mirrors hexagonal architecture.