I discovered ClojureDart recently and used it to make a simple mobile app for tracking time outdoors. The ClojureDart community is somewhat fledgling, so I was pleasantly surprised to see "Why Flutter needs Clojure" and learn of the nest
macro. In this post, I'd like to develop the nest
macro to a couple more levels of sophistication.
While web UIs configure padding, margin, and background color with CSS, Flutter uses container components. Adding a background color to a component requires wrapping the component in a Container
and setting the original component as the value of the container's child
property. To add padding, wrap the component in Padding
. This isn't impractical, though it can be slightly annoying to indent code further and further as more ancestors are added, but it wasn't a big enough problem for me to think of writing a macro to solve it. That said, a macro to nest components into a .child
hierarchy is delightfully simple:
(defmacro nest [form & forms]
(let [[form & forms] (reverse (cons form forms))]
`(->> ~form
~@(for [form forms]
(-> form
(cond-> (symbol? form) list)
(concat ['.child])
(with-meta (meta form)))))))
(macroexpand '(nest a b c d))
(a .child (b .child (c .child d)))
Actual code looks more like this:
(nest
(m/Container .color m/Colors.red)
(m/Padding .padding (m/EdgeInsets.fromLTRB 0 0 10 0))
(m/Row .mainAxisAlignment m/MainAxisAlignment.end
.children [(m/Icon m/Icons.delete .color m/Colors.white)]))
A minor added benefit is that inserting a new ancestor component changes fewer lines, as it doesn't indent all its descendant components.
Notice that nest
doesn't nest .children
, only .child
. The difference is mere plurality, so let's create a new macro that nests the last value under .children
when the last value is a vector. I'll call my revised macro >>
, since I think of it similarly to the threading macro ->>
:
(defmacro >> [& forms]
(let [form (last forms)
forms (butlast forms)
forms (if (vector? form)
(concat (interpose '.child forms) ['.children])
(interleave forms (repeat '.child)))
value-props (reverse (partition 2 forms))]
`(->> ~form
~@(for [[form prop] value-props]
(-> form
(cond-> (symbol? form) list)
(concat [prop])
(with-meta (meta form)))))))
(macroexpand '(>> a b c [d]))
(a .child (b .child (c .children [d])))
Note that this doesn't work if the last value is only a vector at runtime; it has to be a vector literal at compile time:
(macroexpand '(>> a b c (into [] [d])))
(a .child (b .child (c .child (into [] [d]))))
Because of this, we may sometimes want to force the nesting property to be .children
, and it may occasionally make sense to nest components under other properties (e.g., under RichText
's .text
property). Thus, let's allow property names to be specified before a form, and only when a property name is missing use .child
:
(>> (a) (b) .text (c) .children (d))
To create such a macro, we need a couple helper functions. Here's the first, which determines whether a symbol denotes a property name (in ClojureDart, Clojure functions needed by macros must be given the metadata :macro-support
):
(defn ^:macro-support prop? [s]
(and (symbol? s) (= \. (first (name s)))))
The second helper function parses a sequence of values by walking it backwards and looking for prop?
symbols to associate with the previous form:
(defn ^:macro-support value-prop-pairs [forms]
(loop [[form & forms] (reverse forms)
prop '.child
pairs []]
(cond
(nil? form)
, pairs
(prop? form)
, (recur forms form pairs)
:else
, (recur forms '.child (conj pairs [form prop])))))
Our earlier >>
macro now only needs minor adjustment:
(defmacro >> [& forms]
(let [form (last forms)
forms (butlast forms)
forms (if (vector? form)
(concat forms ['.children])
forms)
value-props (value-prop-pairs forms)]
`(->> ~form
~@(for [[form prop] value-props]
(-> form
(cond-> (symbol? form) list)
(concat [prop])
(with-meta (meta form)))))))
Let's test it. Are vectors in the final position still placed under .children
?
(macroexpand '(>> (a) (b) .text (c) [d]))
(a .child (b .text (c .children [d])))
Can non-vectors be specified as .children
?
(macroexpand '(>> (a) (b) .text (c) .children (d)))
(a .child (b .text (c .children (d))))
Can vectors be specified as .children
?
(macroexpand '(>> (a) (b) .text (c) .children [d]))
(a .child (b .text (c .children [d])))
And finally, can a vector in the final position be placed under a different property that .children
?
(macroexpand '(>> (a) (b) .text (c) .friends [d]))
(a .child (b .text (c .friends [d])))
The behavior in that last case is handled by value-prop-pairs
walking forms in reverse order. Even though when a vector is in the final position we append a '.children
to the list of forms, it's dropped in favor of any property symbols before it. This makes >>
very forgiving, perhaps undesirably so:
(macroexpand '(>> (a) (b) .text (c) .friends .romans .countrymen [d]))
(a .child (b .text (c .friends [d])))
If you have suggestions for improvements, or leveraging ClojureDart further, I'm happy to hear them. Feel free to email me.