5 Purity: Avoiding mutable state
This chapter argues that much of software complexity and many bugs stem from mutable state shared across components. It introduces purity and the related concept of referential transparency as the foundation for reasoning about programs: expressions should be replaceable by their values without altering observable behavior. While real programs must interact with the world, the goal is to minimize mutations and isolate side effects, structuring systems so a pure, deterministic core operates over immutable data and a thin boundary handles I/O and other effects.
Through examples, the text shows how shared mutation undermines correctness and composability. A seemingly straightforward average computation over a movie’s scores can yield inconsistent results if the list changes mid-calculation or if redundant, interdependent fields (like a cached size) drift out of sync. Making data immutable removes these hazards. Purity is clarified via referential transparency: a max function that logs to an error stream is not pure because replacing the call with its value changes program behavior. The chapter demonstrates how to “program without side effects” by returning new values rather than changing existing ones (e.g., computing a player’s next position in a maze), and recommends separating a pure state-transition core from the presentation and event loop. For larger systems, pure functions can return “what to change” descriptions that an effectful layer applies, keeping the boundary between pure and impure code explicit.
Concurrency magnifies the risks of shared mutation: even decrementing a shared counter can race, and mutexes—while sometimes necessary—serialize execution and cap scalability (per Amdahl’s law). Safer choices are to avoid sharing, use immutable data, or confine mutability to single owners. C++ provides tools to enforce these designs: const and constexpr restrict mutation; const-correctness for references, pointers, and member functions treats the implicit this as const to prevent unintended changes; logical and internal constness can coexist, with carefully controlled mutable members enabling thread-safe caching. The chapter also covers performance-aware immutability (ref-qualified “with_” methods that move from temporaries) and cautions about pitfalls: const can inhibit moves when return-value optimizations don’t apply, and “shallow const” on pointer members leaves pointees mutable unless disciplined (e.g., via pointer wrappers). The overarching guidance is to favor const pervasively, isolate side effects, and introduce controlled mutability only where it is demonstrably beneficial.
The result of the calculation depends on the order of evaluation of the division operands and on the place where the new item is inserted while you’re calculating the average score.
The maze is defined by a matrix in which each cell can be a wall or a hallway. The player can move around but can’t go through the walls.
Two concurrent processes want to decrement a value. Each needs to retrieve the value from memory, decrement that value, and store it back into memory. Because the initial value is 2, you expect the result after decrementing it twice to be 0. But if the second process is allowed to read the value from memory before the first process has stored its result back into memory, you may end up with a wrong result.
It would be ideal to have a tenfold speedup when increasing the number of processors 10 times. Unfortunately, the speedup doesn’t increase linearly with the number of processors. According to Amdahl’s law, if you have only 5% of your code serialized (through mutexes or other means), the most speedup you can achieve compared to running the code on a single processor is 20—even if you throw in 60,000 processors.
You can have both mutable and immutable states; you can share data or not. The only option that produces problems in the concurrent environment is when the data is both shared and mutable. The other three options are safe.
Summary
- Most computers have multiple processing units. When writing programs, you need to ensure that your code works correctly in multithreaded environments.
- If you overuse mutexes, you’ll limit the level of parallelism your program can achieve. Because of the need for synchronization, program speed doesn’t grow linearly with the number of processors you’re using.
- Mutable state isn’t necessarily bad; having it is safe as long as it isn’t shared between multiple system components at the same time.
- When you make a member function
const, you promise that the function won’t change any data in the class (not a bit of the object will change), or that any changes to the object (to members declared asmutable) will be atomic as far as the users of the object are concerned. - Copying the entire structure when you want to change only a single value isn’t efficient. You can use special immutable data structures to remedy this (chapter 8 covers immutable data structures).
Functional Programming in C++ ebook for free