What I Learned on the Way to Clojure

Clojure Logo

Recently I had my first experience writing Clojure for a real world desktop application. Previous to this experience I had only written a handful of scripts and read a couple of books (Programming Clojure and The Joy of Clojure).

One of the challenges I faced writing Clojure in a complex desktop application versus contrived exercises was the apparent necessity of mutable state. Our application had configuration state that was loaded, updated, and saved. Moreover, certain data in the application could be updated throughout the session and affected a number of areas in it.

Clojure does not strictly prohibit state and it even has easy-to-use programming constructs to support it that are thread safe and even transactional. In the words of Uncle Ben, “with great power comes great responsibility.” It was important for me to reflect on whether mutable state was the right choice. And if it is, what’s the best approach to dealing with mutable state?

Lastly, I found myself speaking in and falling back on imperative programming patterns, and it was yet another challenge to move past it and embrace Clojure properly.

When Using Mutable State…

In Clojure, updating state or accessing the value of a reference is very deliberate. The deref/@ operator must be used to dereference a reference type and a combination of dosync and ref-set functions must be used to update it.

Needless to say, it is obvious when a reference is being accessed or updated. I (and others at Atomic) appreciated the deliberate approach to mutating state in Clojure and made sure to extend that into our own development patterns.

Rather than placing mutable state in a number of different locations (namespaces, files), we kept state in well-defined “holder function” bags — know who and where your enemy is. If a function mutated state, we ensured that its name reflected it by ending the function with ! and starting it with set.

For example,

(def config-file (ref nil))

(def data (ref []))

(defn -main [& args]
  (let [{:keys [config] inputs}] (cli args ["--config" "Config file"])
    (dosync (ref-set config-file config))
    (println @config-file)
    (dosync (ref-set data [1 2 3]))
    (println @data)))

The snippet of code above doesn’t encapsulate reference variables (in neither a namespace or function) and leaves it to the caller to decide how it’s dereferenced. We may, for instance, want to ensure that the reference is dereferenced in a transaction by using the ensure function.

Instead, we’d favor something like this:

(ns com.atomicobject.state
  
  (def ^:private config-file* (ref nil))

  (defn config-file [] (@config-file*))

  (defn set-config-file! [new-config]
    {:pre? [(string? new-config)]}
    (dosync
      (ref-set config-file new-config)))

  (def ^:private data* (ref []))

  (defn data [] (@data*))

  (defn set-data! [new-data]
    {:pre? [(coll? new-data)]}
    (dosync
      (ref-set data new-data))))

(defn -main [& args]
  (let [{:keys [config] inputs}] (cli args ["--config" "Config file"])
    (state/set-config-file! config)
    (println (state/config-file))
    (state/set-data! [1 2 3 4])
    (println (state/data)))

This snippet places mutable state into a well-defined namespace and encapsulates access to it through a set of functions.

The Imperative Temptation

It’s easy to fallback on imperative programming constructs that feel comfortable, and it’s even easier to speak in imperative terms (for/while loop, switch statement, etc.). It’s important to embrace idiomatic Clojure. Take the time to re-wire your brain to speak in terms that make sense in a functional language that favors immutable data structures, first class functions, and recursive algorithms.

For instance, the for function in Clojure is not a language construct that mutates a local variable and executes a set of code repeatedly until a certain condition is met (it isn’t a ‘loop’). Instead, it’s a function that binds a variable to sequence (or vector), evaluates a set of expressions, and recurses until the sequence is fully evaluated or a certain condition is met.

Taking the time to even speak in those terms, I found, was an important first step in “doing functional”.

The Journey

It takes time and practice to properly use a language that is predominately functional. It is important to embrace the learning experience and the journey that comes along with learning a new language. Be idiomatic, reserve judgement, hang out with the community surrounding Clojure, take risks, make mistakes, find an excuse to use it, and have fun with it.