Jul 12, 2011

In the quest for programming nirvana developers are constantly trying to reduce complexities in their code. One source of confusion and complexity is mutation. This post is about the different faces of mutation and state change, and the ways that Clojure helps to alleviate the complexities surrounding them. This is not a comprehensive treatise by any means, but hopefully it serves as a survey.

disclosure: this was a rejected submission to PragPub’s Special Clojure issue (which was excellent BTW), so it’s much longer than I would have liked for my blog, and probably much more formal than I normally write.

A surfeit of mutation

The Java programming language allows one to create classes with publicly accessible fields:

package gfx; public class ThreeDee { public double x; public double y; public double z; }

This level of promiscuity in a Java class definition allows any other piece of code to directly manipulate the instance fields:

ThreeDee p = new ThreeDee(); p.x = 0.0; p.y = 1.0; p.z = 2.0;

Almost every Java book written will discourage public class fields in the name of tight-coupling and instead promote the use of getters and setters. From the perspective of mutation complexities however, there is very little difference between one and the other. The tangled web of mutation still exists.

While this example is extreme, Java’s model for mutation leads to a tightly coupled web of mutation that can make programs difficult to reason about, test, and change. There are better alternatives to this web of insanity, as I will discuss next.

Package local mutation

A more constrained model of mutation is one bounded by package access. Observe the following class definition:

package gfx; public class ThreeDee { double x; double y; double z; }

The class ThreeDee now limits access to its fields to only the classes within the gfx package. This is less of a problem for coupling because the assumption is such that you have more control over the code in a given package and can therefore adjust accordingly should the fields in ThreeDee change. However, while the web of mutation has been shrunk, it is still complex within the gfx package itself.

While certainly not a widespread phenomenon, you will on occasion encounter package-level mutable fields in Java source in the wild.

Class local mutation

Every Java book (and most OO books in general — for good reason IMO) written will espouse the virtues of data hiding encapsulation. This practice is useful not only in hiding implementation details, but also to hide mutation. For example, Google’s Guava library provides an ImmutableList class that implements an immutable implementation of the classic list data-structure. An example usage is as below:

import com.google.common.collect.ImmutableList; ImmutableList lst = ImmutableList.of("servo", "joel", "crow"); System.out.println("lst is " + lst.toString()); System.out.println("REVERSING!"); lst.reverse(); System.out.println("lst is now " + lst.toString());

When running the code above, you’ll notice the following printed:

lst is [servo, joel, crow] REVERSING! lst is [servo, joel, crow]

The ImmutableList class lives by its namesake and does not provide a mutable interface, in fact if you try to call the mutable bits of the java.util.List interface a java.lang.UnsupportedOperationException is thrown.

lst.remove(0); // java.lang.UnsupportedOperationException

The Guava library is designed to provide clean immutable collections1 for use in Java programs. However, that’s not to say that mutation isn’t there. Instead, the mutable bits are cleverly hidden away inside of the Guava classes. In the case of ImmutableList there is a plain-old Java array holding the elements of the list hidden away from grubby little mutants.

Limiting mutation at the class boundary is a fairly nice way to develop Java classes, especially those requiring concurrent execution. That is, if a class is immutable from an external API perspective and its internals are thread-safe, then instances can be shared freely across thread boundaries.

However, there is still a problem. That is, Google’s Guava library is an amazing piece of programming, but it’s advantages can only be realized within the context of a system-wide convention for immutability. In other words, Java will not enforce immutability as a language feature — the onus is on us to enforce our own best-practices.

I don’t know about you, but I’ve found programming conventions and best-practices difficult to observe completely when left to my own devices.

This is where Clojure enters the fray.

Single points of mutation

Clojure is a programming language in the Lisp family of languages that eschews promiscuous mutation. The core libraries and data structures are geared toward immutability by default. In fact, Clojure’s data-structures provide most of the same functionality as Guava, except in Clojure these features are exposed and enforced at the language level. Therefore, the problem of an adherence to convention is simplified vastly. However, let’s be realistic; sometimes mutation is needed. In the case where mutation really does seem like a good fit for any given problem at hand, Clojure provides multiple solutions.

Reference types

Recall the image denoting the aforementioned web of mutation:

Clojure’s model of mutation attempts to simplify the model from the chaos above by distilling the points of mutation into as few points of evil as possible; preferably one:

Clojure offers a set of reference types that provide a mutation model centered on singular points of mutation. References types can be viewed as containers for values — where values are things that cannot be changed like the number 9 or the immutable vector [1 2 3] . Clojure therefore allows mutation only at the boundary of the reference type under very specific semantic constraints. The precise mutation semantics of each reference type are beyond the scope of this article,2 but common among each is that the mutation occurs as the result of a function call given the reference type’s current value.

Atoms

The simplest of Clojure’s mutable reference types is the Atom. Simply put, the Atom implements thread-safe compare-and-swap logic for mutation.

(def TIME (atom 0))

The Atom TIME when created will hold the value 0 . To get at the value inside Clojure provides a dereferencing function deref (note, the symbol ;=> denotes a function return value):

(deref TIME) ;=> 0

All of Clojure’s reference types adhere to a simple interface for retrieving their value using deref (or the syntactic @ operator, that does the same thing). To update the value in the Atom TIME Clojure uses the swap! function that itself takes the Atom and a update function that will be used to calculate a new value from the current value:

(swap! TIME inc) @TIME ;=> 1

You can also pass arguments to the update function (where applicable):

(swap! TIME + 100) @TIME ;=> 101

Internal to the swap! function, the preceding will be executed as such:

Get the current value of TIME from the Atom Calculate (+ <current value> 100) Check if the value in the Atom is the same as before If it is, then set the new value to the calculated value Else Retry from step 1

Pretty simple no? It is when provided as a language-level feature, but to implement compare-and-swap correct and efficient in the context of a large codebase is a decidedly more complex task.

As mentioned, Atoms are but one mutable reference type provided by Clojure. I encourage you to explore the other offerings available: Refs, Vars, and Agents.

Function local mutation

You can also restrict mutation to occur only within the confines of a single function.3 Observe a naive implementation of zencat that uses an internal array to build the return vector:

(defn zencat2 [x y] (let [sz-x (count x) sz-y (count y) ary (make-array Object (+ sz-x sz-y))] (dotimes [i sz-x] (aset ary i (nth x i))) (dotimes [i sz-y] (aset ary (+ i sz-x) (nth y i))) (vec ary)))

Aside from being highly inefficient, the function zencat2 is filled to the brim with mutation. However, the mutable array ary never escapes the scope of the zencat2 function. Instead, it is converted to an instance of Clojure’s immutable vector. There will come a time when you may need to implement an algorithm that requires mutation either for speed or the sake of clarity. That’s OK, Clojure will help you to hide the necessary mutations in a lovely veneer.

Immutable locals

The function zencat2 used a mutable array instance to build a longer sequence from the concatenation of two other sequences. This fact gave the illusion of mutable locals, but in reality locals in Clojure are immutable by default. This fact can be daunting if you come from a language where mutation runs rampant. The natural question that arises when one realizes the fact of immutable locals is, “how do you keep local state?” As it turns out, the answer has already been revealed in the implementation of zencat . That is, in languages like Clojure and Erlang (to name only two) that do not have mutable function locals, the way to “change” the values of locals is through recursion. In the case of zencat , the value of the loop locals src and ret are changed to their new values on each invocation of the recur (implementing tail-recursion) statement. Therefore, it is possible, and in most cases preferable, to eliminate mutability entirely in Clojure. Observe the following:

(defn zencat3 [x y] (if (seq y) (recur (conj x (first y)) (next y)) x))

The implementation of zencat3 contains not a single mutation! The manipulation of immutable data-structures is really Clojure’s strong points, and in fact is highly optimized to support such manipulations as idiomatic. The majority of the Clojure code that you will find in the wild will look similar to zencat3 , but there is still unneeded complexity in its implementation because as we know, recursion is a low-level operation.

Point-free

While the surface area of mutation has been eliminated with the implementation of zencat3 , there is still complexity in that you need to reason through the different values that x and y take as the recursive invocations execute. What if you could eliminate x and y completely? As it turns out, you can. Functional programming fosters another style referred to as “point free” style. In a nutshell, point-free refers to the act of composing existing functions in such a way that their arguments are never explicitly listed. An implementation of zencat4 using point-free style would work as follows:

(def zencat4 (comp vec concat))

The comp function is a highly useful tool for composing functions in a point-free style. Simply put, comp returns a function that is the composition of the functions vec and concat . In other words, comp effectively returns the following:

(fn [x y] (vec (concat x y)))

However, rather than cluttering the implementation of zencat4 with references to locals x and y , we can read the point-free implementation as; make a vector out of the concatenation of the arguments to zencat4 . Point-free style is not always the best approach, but in some cases it can truly provide elegant solutions, and from my perspective represents the functional programming ideal:

Build independent, generalized, state-free functions

Plug them together via composition

This approach also works from the consumer’s perspective. Whereas, for any desired function Z composed of functions A . B , one can understand Z simply by first understanding B , followed by A .4 Point-free style is the capstone realization of true reusability, but it only works well when a programming language provides powerful abstractions — a separate topic for another day.

Clojure has it all

We’ve travelled a twisty passage through the different manifestations of mutation. Starting with a wide-open mutation strategy and ending on implementation strategy without a mutation in sight, nor even a local spied. Clojure is an opinionated language with regards to mutation, but as we saw, its idioms allow for a wide range of mutation styles. While it would be beautiful and pure to disallow mutation in any of its forms, Clojure is a practical language and therefore provides the tools for mutation should requirements dictate the need for them. The title of this post is provocative (and evocative for others) in the spirit of fun. Neither myself, nor does the Clojure ideal deny the need for mutability. Clojure provides support for a wide spectrum of options for mutation, it’s up to us to use its offerings wisely.

:F