Now that we’ve explored the landscape of some of the common problems of concur- rent programs and shared state, including the popular solution of locks, we’re ready to examine an alternative point of view. Let’s begin by reexamining a construct offered by most popular programming languages to deal with state—that of objects.
OO languages like Java, C++, Ruby, and Python offer the notion of classes that contain state and related operations. The idea is to provide the means to encapsulate things to separate responsibility among various abstractions, allowing for cleaner design. This is a noble goal and is probably even achieved once in a while. But most languages have a flaw in this philosophy that causes problems when these same programs need to run as multithreaded applications. And most programs eventually do need multithread- ing, either because requirements change or to take advantage of multicore CPUs.
The flaw is that these languages conflate the idea of what Rich Hickey calls identity with that of state. Consider a person’s favorite set of movies. As a child, this person’s set might contain films made by Disney and Pixar. As a grownup, the person’s set might contain other movies, such as ones directed by Tim Burton or Robert Zemeckis.
The entity represented by favorite-movies changes over time. Or does it?
In reality, there are two different sets of movies. At one point (earlier), favorite- movies referred to the set containing children’s movies; at another point (later), it referred to a different set that contained other movies. What changes over time, there- fore, isn’t the set itself but which set the entity favorite-movies refers to. Further, at any given point, a set of movies itself doesn’t change. The timeline demands different sets containing different movies over time, even if some movies appear in more than one set.
To summarize, it’s important to realize that we’re talking about two distinct con- cepts. The first is that of an identity—someone’s favorite movies. It’s the subject of all the action in the associated program. The second is the sequence of values that this identity assumes over the course of the program. These two ideas give us an interesting definition of state—the value of an identity at a particular point time. This separation is shown in figure 6.1.
favorite-movies (identity)
Disney movies (value)
Pixar movies (value)
Other movies (value) At childhood
At adolescence
At adulthood
Figure 6.1 It’s important to recognize the separation between what we’re talking about (say, favorite movies, which is an identity) and the values of that identity. The identity itself never changes, but it refers to different values over time.
This idea of state is different from what traditional implementations of OO languages provide out of the box. For example, in a language like Java or Ruby, the minute a class is defined with stateful fields and destructive methods (those that change a part of the object), concurrency issues begin to creep into the world and can lead to many of the problems discussed earlier. This approach to state might have worked a few years ago when everything was single threaded, but it doesn’t work anymore.
Now that you understand some of the terms involved, let’s further examine the idea of using a series of immutable values to model the state of an identity.
6.2.1 Immutable values
An immutable object is one that can’t change once it has been created. To simulate change, you’d have to create a whole new object and replace the old one. In the light of our discussion so far, this means that when the identity of favorite-movies is being modeled, it should be defined as a reference to an immutable object (a set, in this case). Over time, the reference would point to different (also immutable) sets. This ought to apply to objects of any kind, not only sets. Several programming languages already offer this mechanism in some of their data types, for instance, numbers and strings. As an example, consider the following assignment:
x = 101
Most languages treat the number 101 as an immutable value. Languages provide no constructs to do the following, for instance:
x.setUnitsDigit(3) x.setTensDigit(2)
No one expects this to work, and no one expects this to be a way to transform 101 into 123. Instead, you might do the following:
x = 101 + 22
At this point, x points to the value 123, which is a completely new value and is also immutable. Some languages extend this behavior to other data types. For instance, Java strings are also immutable. In programs, the identity represented by x refers to different (immutable) numbers over time. This is similar to the concept of favorite- movies referring to different immutable sets over time.
6.2.2 Objects and time
As you’ve seen, objects (such as x or favorite-movies) don’t have to physically change for programs to handle the fact that something has happened to them. As discussed previously, they can be modeled as references that point to different objects over time. This is the flaw that most OO languages suffer from: they conflate identities (x or favorite-movies) and their values. Most such languages make no
141 Separating identities and values
distinction between an identity such as favorite-movies and the memory location where the data relating to that identity is stored. A variable kyle, for example, might directly point to the memory location containing the data for an instance of the Person class.
In typical OO languages, when a destructive method (or procedure) executes, it directly alters the contents of the memory where the instance is stored. Note that this doesn’t happen when the same language deals with primitives, such as numbers or strings. The reason no one seems to notice this difference in behavior is that most lan- guages have conditioned programmers to think that composite objects are different from primitives such as strings and numbers. But this isn’t how things need to be, and there is another way. Instead of letting programs have direct access to memory loca- tions via pointers such as favorite-movies and allowing them to change the content of that memory location, programs should have only a special reference to immutable objects. The only thing they should be allowed to change is this special reference itself, by making it point to a completely different, suitably constructed object that’s also immutable. This concept is illustrated in figure 6.2.
This should be the default behavior of all data types, not only select ones like num- bers or strings. Custom classes defined by a programmer should also work this way.
Now that we’ve talked about this new approach to objects and mutation over time, let’s see why this might be useful and what might be special about such references to immutable objects.
Initial value
Value at time = T1
Value at time = T2
Value at time = T3
Value at time = T4
Value at time = T5 Initial pointer
Pointer at time = T1
Pointer at time
= T2
Current pointer Ref
Figure 6.2 A reference that points to completely different immutable values over time
6.2.3 Immutability and concurrency
It’s worth remembering that the troubles with concurrency happen only when multi- ple threads attempt to update the same shared data. In the first part of this chapter, we reviewed the common problems that arise when shared data is mutated incorrectly in a multithreaded scenario. The problems with mutation can be classified into two gen- eral types:
■ Losing updates (or updating inconsistent data)
■ Reading inconsistent data
If all data is immutable, then we eliminate the second issue. If a thread reads some- thing, it’s guaranteed to never change while it’s being used. The concerned thread can go about its business, doing whatever it needs to with the data—calculating things, displaying information, or using it as input to other things. In the context of the example concerning favorite movies, a thread might read someone’s favorite set of movies at a given point and use it in a report about popular movies. Meanwhile, a sec- ond thread might update a person’s favorite movies. In this scenario, because the sets are immutable, the second thread would create a new set of movies, leaving the first thread with valid and consistent (and merely stale) data.
We’ve glossed over some of the technicalities involved in ensuring that this works, and we’ll explore Clojure’s approach in much greater depth in the following sections.
In particular, threads should be able to perform repeated reads correctly, even if another thread updated some or all of the data. Assuming things do work this way, the read problem in a multithreaded situation can be considered solved. It leaves only the issue of when two or more threads try to update the same data at the same time.
Solving this second problem requires some form of supervision by the language runtime and is where the special nature of references comes into play. Because no identity has direct access to the contents of various memory locations (which in turn contain the data objects), the language runtime has a chance of doing some- thing to help supervise writes. Specifically, because identities are modeled using special references, as mentioned previously, the language can provide constructs that allow supervised changes to these indirect references. These constructs can have concurrency semantics, thereby making it possible for multiple threads to update shared data correctly. The semantics can ensure more than safe writes; they can signal errors when writes fail or enforce certain other constraints when a write is to be made.
This isn’t possible in most other popular languages today, because they allow direct access to (and mutation of) memory locations. A language that satisfies two require- ments can hope to solve the concurrency problem: the first is that identities not point directly to memory locations but do so indirectly via managed references, and the sec- ond is that data objects themselves be immutable. The separation of identity and state is the key. You’ll see Clojure’s flavor of this approach over the next few sections.
143 Clojure’s way