A macro I'm proud of

I thought this week I’d simply share a little code a wrote somewhat recently. In the process I’ll share how I handle frontend state management currently. It’s not the fanciest solution out there, but it’s clean enough for Findka’s codebase (about 2.5K lines on the frontend at the moment).

First, I stopped storing everything in a single state atom a while ago. I used to do something like this:

(defonce state
  (atom
    {:foo "default value for foo"
     :bar 1}))

(defonce foo (rum.core/cursor-in state [:foo]))
(defonce bar (rum.core/cursor-in state [:bar]))

but then decided that was silly, and switched to just

(defonce foo (atom "default value for foo"))
(defonce bar (atom 1))

As I am quite lazy, I wrote a macro to type out the defonces and the atoms for me (I am fond of this simple macro, but it isn’t the macro I’m referring to in the subject):

(defmacro defatoms [& kvs]
  `(do
     ~@(for [[k v] (partition 2 kvs)]
         `(defonce ~k (atom ~v)))))

(defatoms
  foo "default value for foo"
  bar 1)

(I have a macro that does something similar for spec definitions too).

The part I really like is for derivations. Rum provides a derived-atom function that works like Reagent’s reaction. However, you have to specify the dependencies explicitly (and you have to provide a unique key for add-watch):

(defonce baz (derived-atom [bar] ::baz
               (fn [bar]
                 (+ bar 3))))

Since Reagent has magic for tracking dereferences, the equivalent is less verbose:

(defonce bar (reagent.core/atom 1))
(defonce baz (reagent.ratom/reaction (+ @bar 3)))

So, about the macro of which I am proud: I have written defderivations which infers derivation dependencies based on the presence of @:

(defderivations
  baz (+ @bar 3)
  ...)

; Expands to:
(do
  (defonce baz (derived-atom [bar] #uuid "..." ; random uuid
                 (fn [bar]
                   (+ bar 3))))
  ..)

Findka as of now has 22 source atoms and and 70 derivations. defderivations aids readability quite a bit. As a small bonus, using @ as a dependency marker means you can evaluate (+ @bar 3) via the repl and get the current value.

Biff’s example app currently uses an earlier version of defderivations that’s less good (it also uses a single state atom… not a big deal since there’s only one cursor). I’ll switch it over to the new version at some point. I haven’t put the source on Github yet (publicly), but here it is, warts and all:

(require '[clojure.walk :refer [postwalk]])

; Adapted from postwalk source
(defn cardinality-many? [x]
  (boolean
    (some #(% x)
      [list?
       #(instance? clojure.lang.IMapEntry %)
       seq?
       #(instance? clojure.lang.IRecord %)
       coll?])))

(defn postwalk-reduce [f acc x]
  (reduce f
    (if (cardinality-many? x)
      (reduce (partial postwalk-reduce f) acc x)
      acc)
    [x]))

(defn deref-form? [x]
  (and
    (list? x)
    (= 2 (count x))
    ; @ expands to ns-qualified deref
    (= 'clojure.core/deref (first x))))

; I keep this in trident.util, but copied here for clarity
(defn pred-> [x f g]
  (if (f x) (g x) x))

(defmacro defderivations [& kvs]
  `(do ~@(for [[sym form] (partition 2 kvs)
               :let [deps (->> form
                            (postwalk-reduce
                              (fn [deps x]
                                (if (deref-form? x)
                                  (conj deps (second x))
                                  deps))
                              [])
                            distinct
                            vec)
                     form (postwalk
                            #(pred-> % deref-form? second)
                            form)
                     k (java.util.UUID/randomUUID)]]
           `(defonce ~sym (rum.core/derived-atom ~deps ~k
                            (fn ~deps ~form))))))

(Why do I always use defonce instead of just using that for source atoms? Early on I ran into a problem where redefining a derivation left the original still running—even when using a static add-watch key, instead of a random UUID—causing performance to grind to a hault, since all the derivations get redefined whenever shadow-cljs evaluates the file again. defonce was a quick fix, though sadly it means I have to hit refresh whenever I redefine a derivation. Some day I’ll figure out the root issue… eventually. As in, “probably never.”)

A caveat of this whole approach is that derivations are calculated depth-first. Suppose A depends on B and C which both depend on D. If D is updated, A might get updated twice: first with an updated value of B but an old value of C, and only after with updated values for both B and C. It’s caused subtle bugs for me once or twice. I think derivatives handles this correctly, though I haven’t read the source.