← Lectures
Back

1 / 7

Next

Slide 1 of 7: The property underneath is a promise about time

Arrow keys move through the slides while these controls have focus.

The theatre is not shown, because your system asks for reduced motion. The lecture itself is below, in full.

Lecture · 7 slides

Immutability, and what it is not free of

Immutability has won the argument, and it won it so completely that the case for it is now made mostly by assertion. Values should not change. Copy rather than modify. Prefer the constant. A language designed today will make the immutable option the shorter one to type, and the people who chose that default were right.

But a technique that has stopped being questioned has also stopped being taught properly, and the version that gets repeated is missing both halves of the reasoning. What immutability actually buys is not "fewer bugs" in general; it is a small number of very specific capabilities, and knowing which ones tells you where it will help. What it costs is not merely "some copying"; the costs are also specific, they are sometimes large, and they land in places that are hard to see until you are there.

This lecture tries to state both sides carefully enough that the default can be applied with judgement rather than as a reflex — which is, in the end, the only way a default is worth anything.

AssumesYou have copied an object to avoid mutating it, and at least once wondered whether that was wasteful.

  1. Slide 1

    The property underneath is a promise about time

    A mutable value is a question whose answer depends on when you ask. An immutable one is a fact.

    javascript

    That sounds like a small thing and it is the whole thing. If p cannot change, then every conclusion you draw about p stays true for as long as you hold it. You do not have to ask who else has a reference. You do not have to wonder whether the check you made at the top of the function is still valid at the bottom.

    Mutation is not evil; it is a decision to make a program's behaviour depend on its history. Sometimes that is exactly right, because the thing being modelled also has a history. The trouble is that history is invisible in source code, and the reader has to reconstruct it from every path that could have led here.

  2. Slide 2

    Sharing without a conversation

    The first thing you buy is the ability to hand a value to somebody without negotiating.

    Passing a mutable structure to a function raises a question that has no syntactic answer: may it keep the reference, and may it write through it? The answer lives in documentation, or in a convention, or in nobody's head at all, and the failures are the ones that survive review — code that was correct until an unrelated caller started holding onto its argument.

    An immutable value has no such question. It can be passed to another thread without a lock, cached without a copy, stored in a map as a key, and returned from a public method without defensive copying. Java's String is immutable for exactly this reason and it is difficult to imagine the language having survived otherwise.

  3. Slide 3

    Comparison becomes cheap, and that is the trick

    The capability that most people meet first without recognising it is that an unchanged value can be identified by its address.

    javascript

    Because nothing may be modified in place, two references being equal is proof that the contents are equal, and that turns an expensive deep comparison into a pointer check. This single fact is what makes user-interface libraries able to skip work: React's rendering, and the store that hangs in this museum, both rest on it. It is also what makes an undo stack trivial — the old value still exists, unchanged, because nothing could have changed it.

    There is a real subtlety here. Reference inequality does not prove that the contents differ, only that a new object was made. A copy that changed nothing looks different, and a program built on this trick can do a great deal of useless work while remaining entirely correct.

  4. Slide 4

    Persistent structures buy back the asymptotics

    The obvious objection is that copying a large structure to change one field is absurd. It is, and the answer is sixty years old.

    clojure

    Neither vector is a copy of the other. Both point into a shared tree, and b allocates only the handful of nodes along one path from root to leaf. Updating one element of a million-element vector touches around seven nodes, not a million. Chris Okasaki's work in the nineteen-nineties gave the field a whole catalogue of these, and Clojure made them the ordinary case rather than an exotic one.

    So the naive complexity argument is answered. What it is answered with is a logarithmic factor and a pointer chase, which brings us to the cost that does not go away.

  5. Slide 5

    The constant factor does not go away

    Structural sharing fixes the asymptotics and leaves the constants, and on modern hardware the constants are what you feel.

    A mutable array is a contiguous block; walking it is the single fastest thing a processor does, because the prefetcher can see where you are going. A persistent vector is a tree of small nodes scattered across the heap. The element count is the same and the cache behaviour is not, and the gap on a tight numeric loop is routinely a factor of five or ten rather than a few per cent.

    There is a second cost that is easy to miss: allocation pressure. A loop that produces a new value per iteration produces garbage per iteration, and while generational collectors are very good at exactly this shape, "very good" is not "free". Latency-sensitive systems notice.

    The usual resolution is not ideological. Immutable at the boundaries and across the design, mutable locally inside a function where nobody can observe it — which is how the shuffle and the sort hanging in the Gallery of Methods are written, and they are not wrong to be.

  6. Slide 6

    Identity is a real thing, not a mistake

    The deepest cost is conceptual, and it is the one that immutability's advocates tend to skip.

    Some things in the world genuinely have identity. A bank account is not its balance; it is a thing that has had many balances. Two accounts holding the same amount are not the same account. A model that replaces an account with a new value on every deposit has to reintroduce identity somehow, usually as a key in a map, and now you have both an immutable value and a mutable mapping from identity to value — which is not simpler, only differently arranged.

    Rich Hickey's framing is the most useful one available: a value is immutable, an identity is a succession of values over time, and a reference is how you observe an identity. Nothing in that arrangement deletes state. It separates the thing that changes from the thing you reason about, and that separation is the actual benefit. Claiming that functional programs have no state is a misdescription that makes the idea harder to teach.

  7. Slide 7

    Where the default belongs

    The practical position is narrower than the slogan and easier to defend.

    Make immutability the default, because the common case is a value that has no business changing and the cost of freezing it is nothing. Make it absolute at any boundary crossed by concurrency, caching or comparison, since those are the three places where the guarantee is doing real work rather than being decorative.

    Allow mutation where it is invisible: a local buffer, an accumulator, a sort in place on an array nobody else holds. This is not a compromise of principle. A function that mutates only what it created and returns a finished value is, from the outside, a pure function, and the property that mattered was always the one visible from outside.

    And when something in the model genuinely has identity, say so, rather than scattering it across a map of frozen records. The purpose of the default is to make the exceptions few and deliberate. A default that cannot be departed from is not a default; it is a constraint, and it will be worked around in ways nobody writes down.

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

  1. 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.
  2. 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.
  3. Functional programming and object orientationTwo ways of arranging a program, what each one genuinely makes easy, and the trade-off underneath the argument that neither side can escape.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.

← All lecturesAtrium