Lecture · 8 slides
Functional programming and object orientation
Almost every argument about functional programming and object orientation is conducted as though one of them were a mistake. It is a strange way to talk about two families of ideas that have each produced decades of working software, and it usually means the two sides are answering different questions without noticing.
This lecture tries to make the disagreement precise enough to be settled in places, and honest enough to be left open where it genuinely is. The claim is not that the two are secretly the same. They are not. It is that the thing they actually differ about is narrower and more interesting than the slogans suggest, and that once you can name it, most of the heat goes out of the argument and something useful is left behind.
We will build up to one specific problem, stated in 1998 and still unsolved in any language you are likely to be paid to write, which explains why both styles keep surviving each other's obituaries.
AssumesYou can read a small program in a language with functions and objects. Nothing else.
Slide 1
Two arrangements of the same information
A program that does anything has both data and operations, and the only real question is which of the two gets to be the outer structure.
type Shape = | { kind: "circle"; r: number } | { kind: "square"; s: number };typescript Object orientation groups by data. All the things you can do to a circle live together, in one place, and that place is called Circle. Functional programming groups by operation. Everything
areaknows how to do lives together, in one place, and that place is calledarea. Both arrangements contain exactly the same information. They differ in what is easy to add later, and it is worth being suspicious of any argument that does not eventually come down to that.Notice that neither arrangement is a claim about the problem domain. Circles and squares do not, in themselves, prefer to be classes. The choice is a claim about the future: about which axis of the program you expect to grow.
Slide 2
What an object actually is
Strip away the syntax and an object is a value that has captured some state and exposes a fixed set of things you may ask it.
function counter(start) { let n = start; return { inc: () => { n += 1; return n; }, read: () => n, }; }javascript There is no class here, no
new, no inheritance, and yet everything an object is famous for is present. The state is private, because nothing outside can namen. The interface is fixed. Two counters do not interfere. You can pass one to a function that knows only that it responds toread, which is polymorphism.This is not a trick, and it is not an argument that objects are unnecessary. It is the observation that closures and objects are two presentations of one idea, which is why the languages that started from opposite ends have been converging for forty years. Smalltalk got closures. Lisp got objects. The museum hangs the Y combinator a few rooms away partly for this reason: it is recursion built out of nothing but functions, and it makes the point that these categories are less fundamental than the vocabulary implies.
Slide 3
The expression problem
Philip Wadler gave the difficulty its modern name in 1998, and the reason it matters is that it is not an opinion. It is a fact about the two shapes.
Suppose your program has a set of cases and a set of operations over them. Adding a new case means touching every operation. Adding a new operation means touching every case. The question is whether your language lets you do both without editing existing code and without losing type safety. Almost none do, and the ones that come close pay for it in ways that most teams decide are not worth it.
What each style does is choose which of the two additions is cheap. This is the whole disagreement, stated without adjectives, and it explains why the argument never ends: the two sides work on codebases that grow along different axes and are each correctly reporting what they see.
Slide 4
Adding a case
If your program grows by acquiring new kinds of things, the object arrangement is genuinely better, and it is not close.
class Triangle: def area(self): return self.b * self.h / 2 def draw(self, canvas): canvas.polygon(self.points())python A new class arrives carrying every operation it needs. Nothing that already worked is opened, recompiled or retested. This is the case the graphical user interface people had in the nineteen-eighties, when every month brought another kind of widget and the set of things you could do to a widget was more or less fixed, and it is why object orientation won that decade rather than merely being fashionable during it.
The same shape appears in the Strategy and Visitor patterns hanging in the Gallery of Patterns, which are, read closely, two different answers to this same question about which axis you expect to move.
Slide 5
Adding an operation
If instead your program grows by acquiring new things to do with a fixed set of kinds, the arrangement inverts and so does the advantage.
perimeter :: Shape -> Double perimeter (Circle r) = 2 * pi * r perimeter (Square s) = 4 * shaskell A compiler is the canonical example. The kinds of node in a syntax tree change once a decade; the passes over them change every week. Writing that as a class hierarchy means every new analysis edits every node class, and a language with exhaustive pattern matching will tell you at compile time exactly which cases you have not handled yet, which a class hierarchy cannot.
This is why compilers, interpreters and data pipelines drift toward the functional arrangement without anyone declaring a philosophy, and why the teams who build them find the object-oriented advice they are given faintly irrelevant.
Slide 6
Mutation is a decision about time
The other half of the argument is not about arrangement at all. It is about whether a value is allowed to change under you.
const next = { ...state, count: state.count + 1, };javascript An immutable value can be shared without a conversation. You can hand it to another thread, cache it, compare it by identity to see whether anything happened, and keep the old one to show a difference or undo a step. None of these are functional-programming luxuries; they are the reason the undo-and-time-travel machinery in a Redux store is thirty lines rather than a subsystem, and the store itself hangs in this museum.
What immutability costs is real too, and gets less airtime. Copying is not free, persistent data structures buy back the asymptotics but not the constant factor, and a graph of mutually referring objects is genuinely awkward to express without identity. Object orientation did not choose mutation out of carelessness. It chose it because a simulation of a changing world is easier to write when the model changes too, which was exactly the problem Simula was invented for.
Slide 7
What each style is honest about
Each tradition is clearest about the failure mode of the other, and that is worth taking seriously rather than dismissing as tribalism.
The functional critique of objects is that shared mutable state makes a program's behaviour depend on history, and history is invisible in the source text. That is true, and it is the single largest source of the bugs that survive review.
The object-oriented critique of functional programming is that a large program has to hold state somewhere, and pushing it all to the edges does not delete it; it moves it into a shape that some codebases find harder to locate, not easier. That is also true, and it is why the honest functional answer is not that state disappears but that it becomes explicit — which is a real improvement, and a smaller one than the rhetoric usually claims.
Slide 8
What is actually settled
A few things are no longer seriously contested, and it is worth separating them from the parts that are.
Immutability by default is settled. Nearly every language designed since 2005 makes it the easier option, and the ones that did not have added it. First class functions are settled; the argument about whether they belonged in Java ended in 2014. Deep inheritance hierarchies are settled in the other direction, and were abandoned by the object-oriented community itself long before anyone else got around to criticising them.
What is not settled is the expression problem, because it cannot be settled by preference. It is a constraint. Any program of reasonable size grows along both axes eventually, and every language you might choose is better at one of them. The useful skill is not picking a side but noticing, early, which way the particular program in front of you is going to grow, and arranging it accordingly — and then being willing to say that you got it wrong when it grows the other way instead, because it often will.
Works in the collection
The arguments above are hanging on the walls of the museum, in one form or another. These are the ones worth looking at next.
The rest of the programme
- Compiled, interpreted, and the space betweenA distinction that stopped describing anything decades ago, why it persists, and what is actually different about the machinery underneath.
- What a compiler actually doesFour jobs in a row, each mechanical, none magic: text to tokens, tokens to a tree, a tree to a judgement, and a judgement to instructions.
- The principles of object orientationEncapsulation, inheritance, polymorphism and SOLID, one at a time: what each actually claims, which held up, and which its own community abandoned.
- Types: what they can and cannot proveA type checker proves one thing about every possible run of your program. Knowing which proposition explains both the enthusiasm and the disappointment.
- Null, and the mistake its inventor apologised forTony Hoare called it his billion-dollar mistake. What was actually wrong with it, what the alternatives cost, and why the fix took forty years to arrive.
- What abstraction costsAbstraction is sold as free and is not. What you buy, what you pay, and how to tell before writing it which of the two is larger.
- Immutability, and what it is not free ofValues that never change buy sharing, comparison and time travel. They are not free, and it is worth knowing where they are expensive before you commit.
- Recursion, and why it feels like cheatingA function that calls itself looks like an unpaid debt. What makes it terminate, what it costs on the stack, and why some problems resist any other shape.
- Big-O, and what it deliberately ignoresComplexity notation throws away constants, hardware and every input you will actually see. Knowing what it discards is what makes the number useful.
- What regular expressions cannot matchThere is a precise boundary around what a regex can recognise. It explains the famous refusal to parse HTML, and why some patterns run forever.
- Concurrency is not parallelismOne is a way of structuring a program, the other a way of executing it. Keeping them apart explains why async helps a web server and threads often do not.
- Errors: exceptions, values, and what each hidesThrowing makes the happy path readable and the failure paths invisible. Returning errors as values does the opposite. Neither side has won.
- Why programs are hard to changeSoftware is called soft because it can be edited. Why editing gets harder every year, what the mechanism is, and which of the usual remedies work.