To understand why, let’s take a look at simplified computer memory. It consists of a series of eight memory cells, each of which holds a bit of information. Every cell has an address, which represents a place. You can reach into that place and change what’s there, but once you change it, any notion of what was there in the past is lost as shown in the figure.
To change a value, the bit at a particular location is flipped. For instance, in the following dramatic re-enactment, we change the value at memory location 8 from 0 to 1.
This simplified model is similar to the model presented by even modern computer memory. Modern memory holds many more bits, and the addressing schemes it uses are more complex, but the core model of memory as a series of cells with an address still holds.
One core abstraction we work with as developers in popular object-oriented languages is mutable object references to mutable objects. This makes memory friendlier to work with in many ways. However, in one important respect, the model that they provide is much like the computer memory we just looked at.
Traditional object-oriented models encourage us to program as if we were using places to store our data, just like the cells in our simple memory model. Many object references may refer to the same place; however, if we modify an object through any of those references, all of the references may eventually see the modification.
For instance, the following diagram is a simple data model of a person, which represents your humble author. Here, I’m modeled as an object with three references pointing at it, which stand in for Java-style object references. For the sake of the model, let’s say I commit a heinous crime involving three yaks, a razor, and an industrial-sized can of shaving cream.
On the run from the law, I need to change my name and address. Doing so using standard object-oriented tools would mutate the object that represents me. This means that my old name and address would be lost, as the following diagram demonstrates!
This is not how human memory works. Just because I changed my name now, doesn’t mean that my past name is just wiped away. We’re used to computers working this way, so the strangeness inherent in this model isn’t readily apparent to the seasoned developer.
This model can have surprising effects. For instance, let’s say that we modified the object through the reference labeled me. What do the other references see?
In the simple case, our program is single-threaded and we might reasonably expect that once the person is modified, all subsequent attempts to view it through any of the references will see the modified value. Even this simple case can be fairly complex, especially for large software systems. Two pieces of code far apart in the codebase may modify the same data without realizing it, which can cause errors that are difficult to debug.
Worst of all is when the references reside in separate threads. In this case, two threads may attempt to modify the person concurrently. This causes all sorts of problems; we’ll discuss how Clojure helps solve them in Chapter 9, Concurrent Programming in Clojure. The traditional solution to these problems involves locks. Programming with locks is notoriously difficult, and has driven many a noble developer to the brink of insanity, or at least a career in marketing.