Channels ▼
RSS

Mobile

The Clojure Philosophy


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, String and java.util.List, and thus the cat function can be called with either type as its first argument — the appropriate implementation will be invoked.

Note that 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.

The expression problem refers to the desire to implement an existing set of abstract methods for an existing concrete class without having to change the code that defines either. Object-oriented languages allow you to implement an existing abstract method in a concrete class you control (interface inheritance), but if the concrete class is outside your control, the options for making it implement new or existing abstract methods tend to be sparse. Some dynamic languages such as Ruby and JavaScript provide partial solutions to this problem by allowing you to add methods to an existing concrete object, a feature sometimes known as monkey-patching.

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.

Clojure philosophy chessboard illustration
Figure 2: The corresponding chessboard layout.

From the figure, you can gather that the black pieces are lowercase characters and white pieces are uppercase. This kind of structure is likely not optimal, but it's a good start. You can ignore the actual implementation details for now and focus on the client interface to query the board for square occupations. This is a perfect opportunity to enforce encapsulation to avoid drowning the client in board implementation details. Fortunately, programming languages with closures automatically support a form of encapsulation to group functions with their supporting data. (This form of encapsulation is described as the module pattern. But the module pattern as implemented with JavaScript provides some level of data hiding also, whereas in Clojure — not so much.)

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 file-component and rank-component functions and the *file-key* and *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.

Conclusion

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.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video