'(functional software)


Om: Removing Boilerplate

For some time I've been writing web application with ClojureScript and the excellent Om library by David Nolen. Persistent data structures, the virtual DOM from React and a browser REPL make for a very powerful toolset.

The Problem

While Om is very powerful, it strives to be as small as possible. That means it ships with (almost) no syntactic sugar and no utilities to automate common tasks.

Consider this simple example utilizing Om and sablono (a templating library allowing you to write HTML with s-expressions):

(defn todo-item [todo]
  (reify
    om/IRender
    (render [_]
     (html
       [:li.todo {:class (:state todo)}
        [:.title (:title todo)]
        [:.text  (:text todo)]]))))

(defn todo-list [todos]
  (reify
    om/IWillMount
    (will-mount [_] (println "will-mount"))
    om/IRender
    (render [_]
      (html
       [:.todo-list
        [:span.count (str (count todos) " Tasks")]
        [:ul
         (om/build-all todo-item todos)]]))))

We define two functions here: todo-item renders a single list-item with a title and a description. todo-list renders a <span> with the count of tasks followed by an <ul>, in which it renders all todo-items. todo-list also implements IWillMount to run some code as soon as the component is mounted into the DOM.

Even in this simple example we can see some boilerplate: The calls to reify, the explicit listing of the protocols IWillMount & IRender, and the usage of the html macro provided by sablono.

Prismatic's Solution: prismatic/om-tools

Prismatic's excellent om-tools library provides a very elegant solution, which allows you to write:

(defcomponent todo-list [todos]
  (will-mount [_] (println "will-mount"))
  (render [_]
    (html
     [:.todo-list
      [:span.count (str (count todos) " Tasks")]
      [:ul
       (om/build-all todo-item todos)]])))

Most notably the interface names and the call to reify can be omitted. While this is a good start, you still have to wrap the contents of render with html if you want to use sablono.

om-tools also allows you to specify constraints on the allowed input-data and -types via its own schema library, as well as other features like mixins. While these are great features, it increases the complexity of the implementation. Sometimes less is simply more suitable

Reimplementing defcomponent

If you strip away the more advanced features (mixins, validation), a defcomponent-like macro is very easy to implement with just a few lines of Clojure.

::: {.ASIDE} Note that this macro doesn't implement the full set of features offered by defn. Multiple arities, docstrings, and the prepost-map isn't supported. This is left as an excercise for the reader. :::

Our goal is to be able to write code like this:

(defcomponent todo-list [todos]
  (will-mount [_] (println "will-mount"))
  (render [_]
    [:.todo-list
     [:span.count (str (count todos) " Tasks")]
     [:ul
      (om/build-all todo-item todos)]]))

This should expand into:

(defn todo-list [todo]
  (reify
    om/IDisplayName
    (display-name [_] "todo-list")
    om/IWillMount
    (will-mount [_]
      (println "will-mount"))
    om/IRender
    (render [_]
      (html
       [:.todo-list
        [:span.count (str (count todos) " Tasks")]
        [:ul
         (om/build-all todo-item todos)]]))))

The implementation IDisplayName is useful when inspecting your application with the React Developer Tools

Implementation

::: {.ASIDE} This post is by no means intended as an introduction to Clojure or macros. The code should be easy to follow, but I don't go into details. One good introduction into the whole Clojure(Script) ecosystem is Living Clojure. :::

We start with a simple macro that emits a defn which implements IRender:

(defmacro defcomponent [name [& args] irender-impl]
  `(defn ~name [~@args]
     (reify
       om/IDisplayName
       (display-name [_] ~(str name))

       om/IRender
       ~irender-impl)))   

We can already use it like this:

(defcomponent todo-list [todos]
  (render [_]
    (html
     [:span "Hello"])))

The next step is allowing arbitrary protocol implementation in the body-part. Thankfully, all protocols in Om follow a simple schema which makes it easy to transform function names to their protocol names.

render       -> IRender
render-state -> IRenderState
will-mount   -> IWillMount

We define us a simple function to transform a function implementation to the respective protocol name:

(defn method->interface [method]
  (->> (clojure.string/split (str (first method)) #"-")
       (map clojure.string/capitalize)
       (apply str "I")
       (symbol "om.core")))

(method->interface '(render [_] ...))
;; => om.core/IRender

This allows us to rewrite the macro like this:

(defmacro defcomponent [name [& args] & impls]
  (let [pairs (mapcat (juxt method->interface identity) impls)]
    `(defn ~name [~@args]
       (reify
         om/IDisplayName
         (display-name [_] ~(str name))

         ~@pairs))))

;; And use it like this:

(defcomponent todo-list [todos]
  (will-mount [_] (println "will-mount"))
  (render [_]
    (html
     [:span "Hello"])))

Now we only need to get rid of the html for render and render-state. This is just another pass in our macroexpansion and best done in another utility function.

There are some things to consider:

We have to make sure that our function ONLY wraps the body of render and render-state. We also have to make sure to only wrap the LAST expression in our render-functions, as html expects just one single form in the macro and we might want to run (assert ...) and other imperative stuff in render or render-state.

(defn wrap-html [method]
  (let [[fname args & body] method]
    (if (contains? #{'render-state 'render} fname)
      `(~fname ~args
               (do
                 ~@(butlast body))
               (sablono.core/html
                ~(last body)))
      method)))

(wrap-html '(render [_] (assert 42) [:div.foo]))
;; => (render [_] (do (assert 42)) (sablono.core/html [:div.foo]))

(wrap-html '(will-mount [_] ...))
;; => (will-mount [_] ...)

Utilizing this in our defcomponent macro will look like:

(defmacro defcomponent [name [& args] & impls]
  (let [pairs (mapcat (comp (juxt method->interface identity)
                            wrap-html)
                      impls)]
    `(defn ~name [~@args]
       (reify
         om/IDisplayName
         (display-name [_] ~(str name))

         ~@pairs))))

And we're done!

We now have a macro which simplifies our initial snippet to:

(defcomponent todo-item [todo]
  (render [_]
    [:li.todo {:class (:state todo)}
     [:.title (:title todo)]
     [:.text  (:text todo)]]))

(defcomponent todo-list [todos]
  (render [_]
    [:.todo-list
     [:span.count (str (count todos) " Tasks")]
     [:ul
      (om/build-all todo-item todos)]]))   

Conclusion

Om is a great piece of software, but (intentionally) leaves out a great deal of syntactic sugar. Some might find this troublesome - I personally prefer the library-based approach.

With Om exposing only a basic, well-defined api, any user can easily bend it to their needs. In that sense it's like Unix: Do one thing, and do it well. (...and play good with others).

I hope this blog post was able to show new users of Om how powerful Clojure(Script) macros can be: Around 25 lines of code allow us to write much more succinct code - and with a centralized definition of your components, you can easily add validation, logging, and anything else super easy, without having to touch every component you ever wrote.

Thanks to Moritz H. (& others) for proofreading and corrections.