This post is biased but I happen to like and agree with my biases so take everything written below with a grain of salt.
Clojure is one of my favorite programming languages because of its philosophy of handling state, functional programming, immutable data structures and of course macros. However, after using component for a project at work, I noticed that my code stopped looking like idiomatic Clojure code and more like OO Java I used to write. While features like reify, defprotocol, deftype, and defrecord exist they exist for the purposes interop with Java, type extensions and library APIs. In my opinion the bulk of Clojure code should strive to utilize functions and be data oriented. Clearly, with around 1,000 stars on GitHub, many people find component useful, but its object-oriented paradigm feels unnatural and at odds with the way Clojure is written. The rising popularity of component alarms me because looking at some of the code I and others have produced leaves little room for idiomatic Clojure. Today I ran across a great blog post by Christopher Bui that reminded me of why I avoid component instead opting for mount. The best part of it is that it included code that enables me to rant by writing code which is my favorite kind of ranting. As an exercise I decided to rewrite Christopher’s component code using mount and I am quite happy with the results. Here’s the description of the original task:
Let’s say you’re planning on building a new service for managing emails in your application. The work involved is taking messages off of a queue, doing some work, writing data to a database, and then putting things onto other queues. Everything will be asynchronous.
My Clojure code using component looks very similar to the one written by the Christopher because protocols and records end up being at the forefront when they should be de-emphasized, as they are in my mount example. Functions, which are in the background using component are featured in the mount code below. I have the full example shipper project on github that models an warehouse system that:
- reads order numbers that are ready to ship off of a warehouse queue
- sends out email notifications to customers
- writes order status changes and emails to DB
- and then sends notifications to postal to start a shipping process Below is all the code on one page with namespace declarations removed. A real runnable version is available in the GitHub repo above.
(defn load-config [path]
(-> path slurp edn/read-string))
;; since not using a real queue emulate one with a chan
(defn qconn [conf] (chan))
(defn qdisc [q] (close! q))
(defn listener [f ch]
(let [stop-ch (chan)]
(go-loop []
(alt!
stop-ch ([_] :no-op)
ch ([msg] (f msg)
(recur))))
{:listener ch :stop stop-ch}))
(defn stop-listener [{:keys [stop]}]
(put! stop :stop))
(defstate config :start (load-config "resources/conf.edn"))
(defstate warehouse-queue
:start (qconn config)
:stop (qdisc warehouse-queue))
(defstate ready-to-ship
:start (chan)
:stop (close! ready-to-ship))
;; connects ready-to-ship to warehouse-queue,
;; since in reality warehouse-queue is "some", potentially blocking, queue
(defstate warehouse-listener
:start (listener #(put! ready-to-ship %)
warehouse-queue)
:stop (stop-listener warehouse-listener))
(defstate postal-queue
:start (qconn config)
:stop (qdisc postal-queue))
(defstate go-postal
:start (chan)
:stop (close! go-postal))
;; connects go-postal to postal-queue,
;; since in reality postal-queue is "some", potentially blocking, queue
(defstate postal-listener
:start (listener #(put! postal-queue %)
go-postal)
:stop (stop-listener postal-listener))
;; "db" and "emailer" in reality will be defined somewhere else
;; at the edges of the application
(defstate db
:start (atom {}) ;; i.e. (create-conn config)
:stop (reset! db {})) ;; i.e. (disconnect db)
;; emulating emailer.. e.g. mailgun, postal, etc..
(defstate emailer
:start (fn [{:keys [to subject body] :as email}]
{:sent-email email}))
;; ---------------------------------
(defn notify-customer [to order-num]
(let [sent (emailer {:to "customer@shipper.com"
:subject (str "Order #" order-num " is ready to ship!")
:body "Dear Luke S., ... Respect, Darth V."})]
(swap! db assoc order-num {:email sent
:status :ready-to-ship}))) ;; in reality it will be a :status update/conj/etc.
(defstate notifier
:start (listener (partial notify-customer go-postal)
ready-to-ship)
:stop (stop-listener notifier))
Outside of ‘defstate’s which work like regular ‘def’ variables everything is a function. In my opinion, the above looks more idiomatic and I find it easier to read than the componentized version. In my experience mount’ed code ends up being shorter as a bonus. A big takeaway from using mount is that you can require defstate variables like you would any other var in a namespace and it just works. Take a look at the repo for examples. Here’s how to interact with the code in boot/lein REPL session.
boot.user=> (require '[shipper.core :refer [db]]
'[shipper.warehouse :refer [ready-to-ship]]
'[clojure.core.async :refer [put!]]
'[mount.core :as mount])
boot.user=> (mount/start)
{:started
["#'shipper.conf/config"
"#'shipper.postal/postal-queue"
"#'shipper.postal/go-postal"
"#'shipper.postal/postal-listener"
"#'shipper.warehouse/warehouse-queue"
"#'shipper.warehouse/ready-to-ship"
"#'shipper.warehouse/warehouse-listener"
"#'shipper.core/db"
"#'shipper.core/emailer"
"#'shipper.core/notifier"]}
boot.user=> @db
{}
boot.user=> (put! ready-to-ship 42)
true
boot.user=> @db
{42
{:email
{:sent-email
{:to "customer@shipper.com",
:subject "Order #42 is ready to ship!",
:body "Dear Luke S., ... Respect, Darth V."}},
:status :ready-to-ship}}
In short I encourage everyone to keep Clojure idiomatic and beautiful and just because your code has state it doesn’t mean you have to abandon the way you structure your programs.