Naturally, by adhering to a strict model of immutability, concurrency suddenly becomes a simpler (although not simple) problem, meaning if you have no fear that an object's state will change, then you can promiscuously share it without fear of concurrent modification. Clojure instead isolates value change to its reference types. Clojure's reference types provide a level of indirection to an identity that can be used to obtain consistent, if not always current, states.
Imperative "Baked In"
Imperative programming is the dominant programming paradigm today. The most unadulterated definition of an imperative programming language is one where a sequence of statements mutates program state. During the writing of this article (and likely for some time beyond), the preferred flavor of imperative programming is the object-oriented style. This fact isn't inherently bad, because there are countless successful software projects built using object-oriented imperative programming techniques. But from the context of concurrent programming, the object-oriented imperative model is self-cannibalizing. By allowing (and even promoting) unrestrained mutation via variables, the imperative model doesn't directly support concurrency. Instead, by allowing a maenadic approach to mutation, there are no guarantees that any variable contains the expected value. Object-oriented programming takes this one step further by aggregating state in object internals. Though individual methods may be thread-safe through locking schemes, there's no way to ensure a consistent object state across multiple method calls without expanding the scope of potentially complex locking scheme(s). Clojure instead focuses on functional programming, immutability, and the distinction between state, time, and identity. But object-oriented programming isn't a lost cause. In fact, there are many aspects that are conducive to powerful programming practice.
Most of What OOP Gives You, Clojure Provides
It should be made clear that we're not attempting to mark object-oriented programmers as pariahs. Instead, it's important that we identify the shortcomings of object-oriented programming (OOP) if we're ever to improve our craft. In the next few subsections we'll also touch on the powerful aspects of OOP and how they're adopted, and in some cases improved, by Clojure.
Polymorphism is the ability of a function or method to have different definitions depending on the type of the target object. Clojure provides polymorphism via both multimethods and protocols, and both mechanisms are more open and extensible than polymorphism in many languages.
Listing Two: Clojure's polymorphic protocols.
(defprotocol Concatenatable (cat [this other])) (extend-type String Concatenatable (cat [this other] (.concat this other))) (cat "House" " of Leaves") ;=> "House of Leaves"
What we've done in Listing Two is to define a protocol named
Concatenatable that groups one or more functions (in this case only one,
cat) that define the set of functions provided. That means the function
cat will work for any object that fully satisfies the protocol
Concatenatable. We then extend this protocol to the
String class and define the specific implementation a function body that concatenates the argument
other onto the string
this. We can also extend this protocol to another type:
(extend-type java.util.List Concatenatable (cat [this other] (concat this other))) (cat [1 2 3] [4 5 6]) ;=> (1 2 3 4 5 6)
So now the protocol has been extended to two different types,
java.util.List, and thus the
cat function can be called with either type as its first argument the appropriate implementation will be invoked.
String was already defined (in this case, by Java itself) before we defined the protocol, and yet we were still able to successfully
extend the new protocol to it. This isn't possible in many languages. For example, Java requires that you define all the method
names and their groupings (known as interfaces) before you can define a class that implements them, a restriction that's known as the expression problem.
A Clojure protocol can be extended to any type where it makes sense, even those that were never anticipated by the original implementor of the type or the original designer of the protocol.
Clojure provides a form of subtyping by allowing the creation of ad-hoc hierarchies. Likewise, Clojure provides a capability similar to Java's interfaces via its protocol mechanism. By defining a logically grouped set of functions, you can begin to define protocols to which data-type abstractions must adhere. This abstraction-oriented programming model is key in building large-scale applications.
If Clojure isn't oriented around classes, then how does it provide encapsulation? Imagine that you need a simple function that, given a representation of a chessboard and a coordinate, returns a simple representation of the piece at the given square. To keep the implementation as simple as possible, we'll use a vector containing a set of characters corresponding to the colored chess pieces, as shown next.
Listing Three: A simple chessboard representation in Clojure.
(ns joy.chess) (defn initial-board  [\r \n \b \q \k \b \n \r \p \p \p \p \p \p \p \p \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \- \P \P \P \P \P \P \P \P \R \N \B \Q \K \B \N \R])
In Listing Three, line 5 indicates lowercase dark, and line 10 indicates uppercase light. There's no need to complicate matters with the chessboard representation; chess is hard enough. This data structure in the code corresponds directly to an actual chessboard in the starting position, as shown in Figure 2.
Figure 2: The corresponding chessboard layout.
The functions in Listing Four are self-evident in their intent (and as a nice bonus, these functions can be generalized to project a 2D structure of any size to a 1D representation which
we leave to you as an exercise) and are encapsulated at the level of the namespace
joy.chess through the use of the
defn- macro that creates namespace private functions. The command for using the
lookup function in this case would be
(joy.chess/lookup (initial-board) "a1").
Listing Four: Querying the squares of a chessboard.
(def *file-key* \a) (def *rank-key* \0) (defn- file-component [file] (- (int file) (int *file-key*))) (defn- rank-component [rank] (* 8 (- 8 (- (int rank) (int *rank-key*))))) (defn- index [file rank] (+ (file-component file) (rank-component rank))) (defn lookup [board pos] (let [[file rank] pos] (board (index file rank))))
On lines 4-5, we calculate the file (horizontal) projection. On lines 7-8, we calculate the rank (vertical) projection. Starting on line 11, we project a 1D layout onto a logical 2D chessboard.
Clojure's namespace encapsulation is the most prevalent form of encapsulation that you'll encounter when exploring idiomatic source code. But the use of lexical closures provides more options for encapsulation: block-level encapsulation (as shown in Listing Five) and local encapsulation, both of which effectively aggregate unimportant details within a smaller scope.
Listing Five: Using block-level encapsulation.
(letfn [(index [file rank] (let [f (- (int file) (int \a)) r (* 8 (- 8 (- (int rank) (int \0))))] (+ f r)))] (defn lookup [board pos] (let [[file rank] pos] (board (index file rank)))))
It's often a good idea to aggregate relevant data, functions, and macros at their most specific scope. You'd still call
lookup as before, but now the ancillary functions aren't readily visible to the larger enclosing scope in this case, the namespace
joy.chess. In the preceding code, we've taken the
rank-component functions and the
*rank-key* values out of the namespace proper and rolled them into a block-level
index function defined with the body of the
letfn macro. Within this body, we then define the
lookup function, thus limiting the client exposure to the chessboard API and hiding the implementation specific functions and forms.
But we can further limit the scope of the encapsulation, as shown in the next listing, by shrinking the scope even more to
a truly function-local context.
Listing Six: Local encapsulation.
(defn lookup2 [board pos] (let [[file rank] (map int pos) [fc rc] (map int [\a \0]) f (- file fc) r (* 8 (- 8 (- rank rc))) index (+ f r)] (board index)))
Finally, we've now pulled all of the implementation-specific details into the body of the
lookup2 function itself. This localizes the scope of the
index function and all auxiliary values to only the relevant party
lookup2. As a nice bonus,
lookup2 is simple and compact without sacrificing readability. But Clojure eschews the notion of data-hiding encapsulation featured
prominently in most object-oriented languages.
Not Everything Is an Object
Finally, another downside to object-oriented programming is the tight coupling between function and data. In fact, the Java programming language forces you to build programs entirely from class hierarchies, restricting all functionality to containing methods in a highly restrictive "Kingdom of Nouns." This environment is so restrictive that programmers are often forced to turn a blind eye to awkward attachments of inappropriately grouped methods and classes. It's because of the proliferation of this stringent object-centric viewpoint that Java code tends toward being verbose and complex. Clojure functions are data, yet this in no way restricts the decoupling of data and the functions that work upon them. Many of what programmers perceive to be classes are data tables that Clojure provides via maps and records. The final strike against viewing everything as an object is that mathematicians view little (if anything) as objects. Instead, mathematics is built on the relationships between one set of elements and another through the application of functions.
We've covered a lot of conceptual ground in this article. If you're still not sure what to make of Clojure, it's okay we understand that it may be a lot to take in all at once. Understanding will come gradually. For those of you coming from a functional programming background, you'll likely have recognized much of the discussion, but perhaps with some surprising twists. Conversely, if your background is more rooted in object-oriented programming, then you may get the feeling that Clojure is very different than what you're accustomed to. Though in many ways this is true, Clojure does elegantly solve many of the problems that you deal with on a daily basis. Clojure approaches solving software problems from a different angle than classical object-oriented techniques, but it does so having been motivated by their fundamental strengths and shortcomings. With this conceptual underpinning in place, we encourage you to explore Clojure further.
Michael Fogus is a software developer with experience in distributed simulation, machine vision, and expert systems construction. He's actively involved in the Clojure and Scala communities. Chris Houser is a primary contributor to Clojure and has implemented several features for the language. This article was adapted from their book The Joy of Clojure: Thinking the Clojure Way.