UP | HOME
dix

Software Curmudgeon

Mumblings and Grumblings About Software

My Clojure Workflow Reloaded Reloaded
Published on Sep 26, 2013 by dix.

Several months ago Stuart Sierra wrote a post on the Relevance blog which I found very interesting. In it, he described a way of doing Clojure development that I wanted to try. At first glance, I was interested in it only because it gave me a good workflow for testing within the Clojure repl. I love Test-Driven development and I have always had problems practicing it well in Clojure. I had previously tried two approaches. To begin with, I tried to make do with simply running lein test in the shell when I needed to run tests. The context switch coupled with the slow JVM start up time made this approach unfeasible.

Moving on from this, I attempted to run tests in the REPL without the use of Stuart’s tools.namespace library. I learned after a few hours of attempting that I was simply too dumb. I have no idea how code loading works in Clojure. I do not know how to reliably reload code and all its dependencies. I never really knew what the state of the system was at any given time. Once I started making use of the workflow suggested by Sierra I was actually able to reliably and quickly run tests in the repl. This drastically improved my feelings about Clojure development.

A lot of my problems are likely related to the fact that I don’t really know what I’m doing. I understand some of the fundamentals around functional programming and LISP. But I do not understand the specific Java-ness of Clojure. I have never written Java more complex than is necessary to maintain the Braintree Java Client Library. I’m certain that for someone with more familiarity with Java, it would be very easy to start up an instance of Jetty when initializing the system and then setting up the ring handler within. I don’t know how to do that. For this reason, I was unable to full commit to this technique. I had to make a compromise. Here is how I init the system and start the app.

(defn system []
  (let [db-url (or (System/getenv "DATOMIC_URL") "datomic:mem://sayc-app")]
    {:db-url db-url
     :handler (sayc-app.web/create-handler db-url)
     :web-port (or (try (Integer/parseInt (System/getenv "PORT"))
                        (catch NumberFormatException _ nil))
                   8080)
     }))

(defn start [system]
  (d/create-database (:db-url system))
  (migrate/migrate (d/connect (:db-url system)))
  (assoc system
         :server (ring/run-jetty (:handler system) {:port (or (:web-port system) 8080)
                                                    :join? false})
         :db-connection (d/connect (:db-url system))))

Since I don’t really understand jetty I have to rely on ring/run-jetty to initialize the server. This means there will be some hidden elements of global state. I wish I could get rid of it and I don’t think this is exactly what Stuart Sierra had in mind, but it works for me.

Other than the benefit of being able to reliably and quickly run tests, there was a different huge benefit. Constructing an application this way leads you to really consider where all of your global state lies. When you initialize the database connection at such a high level, it is much harder to rely on it deep within the application. This is what led me to the approach I outlined in my previous post. When you initialize this complex system at such a high level, you have to think much harder about what needs access to what.

It is however not all sunshine and roses. This way of working is much harder than just hacking out some Clojure code. It is a quite heavyweight solution. There are parts of it that I found useful that I would like to separate out. I love the good code reloading and I love the ease with which I could run tests. However, designing an application in this way is definitely overkill for the simple toy app I’ve been working on. Granted, I don’t think anyone would have ever thought otherwise. I’m certain that when one is waist-deep in an extremely complicated application this workflow is invaluable.