Functional Programming Patterns
The Core Claim
Functional programming makes one strong claim: most bugs come from state that changes in ways you didn't expect. If you eliminate mutation — make data permanently immutable, make functions produce new values instead of changing existing ones — an entire category of bugs becomes structurally impossible.
This is not a philosophy. It is an engineering tradeoff with measurable consequences.
Array.sort mutates in place. The function accepted data and silently rearranged it. The caller is now working with a different array than the one they passed in — and they have no way to know.
Pure Functions — The Foundation
A pure function has two properties: given the same inputs it always returns the same output, and it produces no side effects. No mutation, no network calls, no logging, no modifying variables outside its scope.
Pure functions compose, cache, parallelise, and test easily. They are the atomic unit of functional programming.
Immutability — Never Change, Always Create
The functional approach to "changing" data is to produce a new version of it, leaving the original intact:
Every operation produces a new value. The original is never touched. Every function that holds a reference to student or scores continues to see exactly what it had — no surprises.
Function Composition — Building Pipelines
If pure functions are atoms, composition is how you build molecules. The output of one function becomes the input of the next:
pipe and compose are trivial to implement but unlock a completely different way of thinking about code. Instead of writing procedures — "do this, then that, then this" — you declare transformations and chain them.
Currying — Functions That Return Functions
A curried function takes its arguments one at a time, returning a new function for each until all arguments are supplied. This enables partial application — fixing some arguments and leaving others open:
Currying transforms functions from "take all arguments now" to "take arguments whenever you have them". This is what makes functions configurable data — you build a specialised tool by fixing the configuration arguments, then pass the tool wherever a simple callback is expected.
Functors — Mappable Containers
A functor is any container that implements map — a way to transform the value inside without leaving the container. You already know the most common one:
Maybe — Safe Navigation Without null Checks
The Maybe monad is the functional replacement for null checks. It represents a value that may or may not exist and propagates absence automatically — no if (x !== null) scattered everywhere:
Without Maybe, this code would need four separate null checks. With Maybe, null propagates automatically — the pipeline just short-circuits to Nothing whenever any step produces an absent value. The transformation functions (getScores, getPhysics) never need to know about nullability at all.
Either — Typed Error Handling
Either is a container with two states: Right (success, the happy path) and Left (failure, carries the error). It is the functional equivalent of try/catch — but composable:
The entire pipeline runs without a single if or try/catch. Errors are values — they flow through the chain the same way successes do, just along the left track.
Transducer — Composition Without Intermediate Arrays
When you chain .filter().map().reduce(), each step creates a full intermediate array. Transducers compose the transformations so the data passes through the entire pipeline in a single pass:
For small arrays the difference is negligible. For large datasets — hundreds of thousands of records, complex pipelines — transducers eliminate allocation pressure entirely.
What You Have Learned
Functional programming is a discipline, not a religion. You do not need to abandon all mutation — you need to know where mutation causes problems and replace it with functional patterns in those places.
The key ideas:
- Pure functions: same input → same output, no side effects — the atomic unit of FP
- Immutability: spread to update, filter/map to modify arrays — originals never touched
- Composition with
pipe/compose: small functions assembled into pipelines - Currying: functions that take arguments one at a time, enabling partial application and reusable specialised callbacks
- Functors: containers implementing
map— transform the value without leaving the container - Maybe monad: null-safe pipelines — absent values propagate automatically, no null checks scattered through logic
- Either monad: typed error handling — errors as values, composable, no try/catch in business logic
- Transducers: compose filter/map logic into a single reducing function — one pass, no intermediate allocation
In the next lesson: Concurrency Patterns — beyond async/await. Task queues, semaphores, rate limiters, and the patterns that let you coordinate dozens of simultaneous async operations without race conditions, deadlocks, or server overload.