10 Minute Vim!

Wrangling State In Clojure

· Read in about 3 min · (433 Words)

“Clojure is immutable, so you can’t change anything, how useless!”

Immutable languages make application state an interesting concept.

In Clojure, you can deal with application state in two main ways. The first way is to pass the state around as parameters to your functions. An example of Pass As Parameter:

(defn delete! [db-con table id]
  (jdbc/delete! db-con table ["id=?" id]))

;; valid-for-delete omitted

(defn delete-user [db-con user-id]
  (if (valid-for-delete db-con "user" user-id)
    (delete! db-con "user" user-id)))

(defn -main [& [connection-string user-id]]
  (let [db-con (make-connection connection-string)]
    (delete-user db-con user-id)))

This requires every function that eventually accesses a database to also have the database connection. The trade-off is one of simplicity: it is easier to test and interact with code that takes all of its dependencies as parameters.

The alternative is to set a thread-safe value somewhere and give the underlying code access to it. In Clojure, the atom primitive is the first choice for this. A common misconception is that Clojure prevents all mutation. The atom primitive can be mutated, it just has to be done with a special swap! function. Let’s call this: Mutate Shared Location.

(def db-con (atom nil))

(defn delete! [table id]
  (jdbc/delete! @db-con table ["id=?" id]))

;; valid-for-delete omitted

(defn delete-user [user-id]
  (if (valid-for-delete "user" user-id)
    (delete! "user" user-id)))

(defn -main [& [connection-string user-id]]
  (swap! db-con (fn [old] (make-connection connection-string)))
  (delete-user user-id))

The atom allows us to not have to pass around the state. We mutate db-con with the connection parameters before calling any database accessing functions. Unfortunately, this sets up an implicit dependency: delete! will only work if the db-con atom was setup beforehand.

Dependencies Correctly Call Function Adding New State Best When
Pass As Parameter Explicit Easier Harder State Values Change Frequently
Mutate Shared Location Implicit Harder Easier State Values Change Rarely

Mutate Shared Location might look familiar, in a lot of other languages it is implemented with the Singleton Design Pattern. Often a Singleton class will act as the mutable shared location for storing state.

When adding new application state, I typically default to Pass As Parameter as my first choice. When Pass As Parameter grows costly, I fall back to Mutate Shared Location. Pass As Parameter works best when the value changes regularly.

An exception would be something as ubiquitous as a database connection in a CRUD application. A CRUD application will typically need a database connection at every leaf node, and it rarely changes, so I will use Mutate Shared Location from the start.

With these two ways of passing application state, we are offered the flexibility to choose the best tool for the job.

steve shogren

software developer, manager, author, speaker

my books:

posts for: