Clojure: Immutability at the Language Level
If you're familiar with the Clojure programming language, then you might know that at its heart lies a powerful set of immutable, persistent collection types. In this article we'll talk a bit about the underpinnings of these collection types, including a deep dive into a couple of them, namely its vectors and maps. We'll wrap up by presenting an example of how viewing a problem through the lens of Clojure we can vastly simplify our design.
|This article is an excerpt from an Early Access Edition of the book "The Joy of Clojure" (Manning Publications; ISBN: 9781935182641), written by Michael Fogus and Chris Houser.|
Why was Clojure designed and implemented with immutability as a cornerstone principle? Although certainly no panacea, fostering immutability at the language level solves many difficult problems right out of the box while simplifying many others. Coming from a language background where mutability interwoven with imperative programming methods reign, we find that Clojure often requires a significant conceptual leap to twist one's mind to accept and utilize immutability and functional programming. In many cases, purists see the essence of immutability as a precondition for any given language to fall into the category of functional programming. Although we're certainly not willing to set hard and fast definitions of such nebulous topics, we'll say that data structure immutability and functional programming do indeed complement one another quite well. In this section we'll build a conceptual basis for immutability as it relates to Clojure's underlying philosophy as well as why you should work to foster immutability even when outside the warming confines of Clojure proper.
When using the word immutability in the context of Clojure, we'll more often than not refer strictly to its core data types. But we can extend the larger picture of immutability beyond the strict ecosystem of data structures alone and instead encompass any object that adheres to the notion of immutability. In many cases, when talking specifically about Clojure's immutable data structures, we could be talking about the broader category of immutable objects without loss of meaning. But in order to make this case, we should probably set down some conditions defining what we mean by immutability.
Every Day Is Like Sunday
An entire branch of philosophy named predestination is devoted to exploring the notion that there's no such thing as free will, but instead, everything that we are or ever will be is determined beforehand. Although this possibility for our own lives may seem bleak, the notion does nicely encapsulate the first principle of object immutability. That is, all of the possible properties of immutable objects are defined at the time of their construction and can't be changed thereafter.
Immutability Exists Through Convention
There's nothing magical about immutable objects. Computer systems are in many ways open systems, providing the keys to the vault if one is so inclined to grab them. But in order to foster an air of immutability in our own systems, it's of utmost importance to create a facade of immutability. Immutability requires that we layer over and abstract the parts of our system that provide unrestrained mutability. For example, creating immutable classes in Java requires us to do this in a number of ways (See Java Concurrency in Practice, by Brian Goetz, for more details). First, a class itself and all of its fields should be labeled as
final. Next, in no way should an object's
this reference escape during construction. And finally, any internal mutable objects should originate, either whole cloth or through a copy, within the class itself and never escape. Obviously, we're simplifying in order to illustrate because there are finer details to this recipe for Java immutability, but for now these simplified highlights serve to show that by observing convention, even an inherently mutable language such as Java can be made immutable. In languages like Java, immutability isn't directly supported and requires some gymnastics in order to achieve, although Clojure directly supports immutability as a language feature with its core data structures (We're intentionally glossing over Clojure's features that support mutability, such as reference types and transients, in order to keep this section focused.). By providing immutable data structures as a primary language feature, Clojure separates the complexity of working with immutable structures from the complexities of their implementation (Reginald Braithwaite discusses this language-level separation of concerns in his excellent blog post "Why Why Functional Programming Matters Matters").
The parts defining immutable objects as outlined here are meant to be viewed agnostic to any specific programming language. But by providing immutability either as a core language feature or through convention, you can buy yourself enormous benefits.
The Benefits of Immutability
Clojure's immutable data structures aren't bolted onto the language as an afterthought or as a choice in an a la carte menu. Instead, their inclusion in the language runs deep to its philosophical core.
Invariant-based programming involves the definition of constraints on classes and functions in order to provide assurances that if its instances enter into certain states, assertion errors will arise. Providing invariants within a mutable system requires a fair amount of assertion weaving within the methods of any given class. But by observing a practice of immutability, invariants are defined solely within the construction mechanism and can never be violated thereafter.
In the face of mutability, passing any object to any function works to obliterate the possibility of tracking the changes in its properties. But because the life of an immutable object is one of predestination, the matter of reasoning about its possible states becomes trivial. It follows that the act of testing such a system is simplified in that the set of the possible states and transitions is constrained.
Equality Has Meaning
Equality in the presence of mutability has no meaning. Equality in the face of mutability and concurrency is utter lunacy. That is, if any two objects resolve as being equal now, then there's no guarantee that they will a moment from now. And if two objects aren't equal forever, then they're technically never equal (For more information about these statements, see Henry Baker's essay "Equal Rights for Functional Objects or, The More Things Change, The More They're the Same."). Providing immutable objects once again assigns meaning to equality in that if two objects are equal now, then they will always be so.
Sharing Is Cheap
If you're certain that an object will never change, then sharing this object becomes a simple matter of providing a reference to it. In Java doing so often requires a lot of defensive copying. Along this vein, because we can freely share references for immutable objects, we can likewise intern them for free.
Flattening the Levels of Indirection
There's a marked difference between a mutable object and a mutable reference. The default in Java is that there are mutable references that contain mutable data. But in Clojure there are only mutable references. This may seem like a minor detail, but it certainly works to reduce unnecessary complexities. Why else is there a proliferation of
final in next-generation Java programming styles?
In a concurrency-oriented programming language like Clojure, the primary benefit of immutability is that the core types can be shared freely among separate threads without fear. In the next section we'll discuss this particular benefit in more detail.
Immutability Fosters Concurrent Programming
Immutable objects are always thread safe.
-- Brian Goetz, Java Concurrency in Practice
If an object can't change, it follows that it can be shared freely between different threads of execution without fear of concurrent modification. There can be little debate about this particular point, but that fact doesn't answer the question of how mutation occurs. Without delving into the specifics, you likely already know that Clojure isolates mutation to its reference types while the data wrapped with them is left unchanged. It's enough to leave this alone for now because we'll devote chapter 9 to this and related topics.