February 22, 20157 min read

Why does functional programming matter?

The title of this post is borrowed, deliberately, from John Hughes’s 1990 classic Why Functional Programming Matters — still one of the most quietly persuasive papers in our field. Its argument is sharper than the usual sales pitch. Functional programming, Hughes says, is not mainly about what it forbids — no assignment, no mutable state, no side effects. A list of prohibitions can’t make a language more powerful. What makes FP matter is what it adds: two kinds of glue — higher-order functions and lazy evaluation — that let you assemble programs out of small, reusable pieces. And modularity, the ability to build big things from small things, is the whole game in maintainable software.

GLUE 1 — HIGHER-ORDER FUNCTIONSfghf ∘ g ∘ hGLUE 2 — LAZY EVALUATIONproducerlazy streamconsumer
FP’s real power is its glue: higher-order functions compose parts; laziness joins producer to consumer.

Purity is testability

Start with the prohibition everyone notices first: no side effects. A pure function’s output depends only on its inputs, which buys you referential transparency — you can replace a call with its result and reason about code like algebra. The practical payoff is testing. A pure function can be verified in complete isolation; there is no hidden global state to set up, no order-of-execution to get right, no spooky action at a distance.

It’s worth noticing that much of the machinery in conventional object-oriented codebases — the sprawling dependency-injection frameworks, the elaborate mocking — exists to claw back, at considerable cost, the testability that purity hands you for free. When state is everywhere, you have to work hard to prove any one module is actually independent. When there is no shared state, independence is the default.

Higher-order functions: the first glue

A higher-order function takes a function as an argument or returns one, and that single capability lets you separate what a computation does from how it’s structured. The traversal of a list and the work done at each step become independent pieces you can mix and match. map, filter, and reduce are the canonical example: the recursion is written once, and the behavior is passed in.

// sum, product, and "any negatives?" are all the same fold,
// differing only in the function glued into it.
const sum     = xs => xs.reduce((a, b) => a + b, 0);
const product = xs => xs.reduce((a, b) => a * b, 1);
const anyNeg  = xs => xs.reduce((a, b) => a || b < 0, false);

This is Hughes’s point made concrete: once iteration is a reusable skeleton, “a new problem” often means “a small new function to glue in,” not a new loop. Higher levels of a program keep full control over behavior while delegating mechanism downward — modularity without a performance tax.

Lazy evaluation: the second glue

The deeper half of Hughes’s argument is lazy evaluation — computing a value only when it’s actually needed. Laziness is the glue that lets you separate generating a structure from selecting from it. You can write a producer that, on paper, yields an infinite amount of data, and a consumer that stops as soon as it has enough, and compose them without either one knowing about the other.

-- an infinite list of primes; only as many are computed as you take
primes = sieve [2..]
  where sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]

-- take 5 primes  ==>  [2, 3, 5, 7, 11]

Hughes’s loveliest examples are numerical: a square root by Newton’s method becomes “generate the infinite sequence of improving approximations, then take the first pair that’s close enough” — the iteration strategy and the stopping condition written as two independent, reusable functions. Laziness is what lets you add a layer of abstraction without paying for the work you don’t use, which is exactly the trade-off that usually makes engineers wary of abstraction.

Why it matters more now: parallelism

Hughes wrote in 1990, before the free lunch ended. Once single-core clock speeds hit a power wall, the industry pivoted to multicore, then to GPUs, then to FPGAs and other accelerators — and suddenly FP’s “restrictions” started looking like gifts. If functions are stateless and data is immutable, there are no race conditions to guard against and no locks to pay for; values can be shared across cores or shipped to a GPU without a second thought. The property that made pure functions easy to test in 1990 is the same property that makes them easy to parallelize today. It’s no accident that map/reduce became the vocabulary of large-scale data processing.

Lambdas as data: LINQ and expression trees

There’s a final twist that Hughes didn’t foresee but would have enjoyed. In multi-paradigm languages like C#, a lambda can be more than code — it can be captured as data, an expression tree the program can inspect and rewrite at runtime. That’s the engine under LINQ: you write a query as ordinary lambdas, and the framework translates that expression tree into SQL for whatever database is behind it, rather than passing an opaque string the compiler can’t check. One language, type-checked end to end, with the translation to other systems handled for you — and, of course, the query executes lazily, only when you enumerate it. Deferred execution and higher-order functions, arriving in the most mainstream of languages by way of the same two ideas.

The quiet victory

Three decades on, the striking thing about Hughes’s paper is how thoroughly it won. You don’t have to write Haskell to feel it: map, filter, and reduce are in every mainstream language; immutability and statelessness are the default advice for anything concurrent or distributed; lambdas are everywhere; and “referential transparency” quietly underwrites everything from React’s render model to the scalability of stateless REST services. Functional programming mattered, in the end, for exactly the reason Hughes claimed: not because of what it takes away, but because better glue makes programs you can take apart and put back together — which is the only kind that survives contact with time.

← All posts