The Clojure standard library is filled with cool macros and functions to smooth Java interoperability and ameliorate the contrast between Java’s verbosity and Clojure’s conciseness, which becomes particularly disconcerting in code that interacts with both languages non-trivially.
I came across one such frustration when writing some Excel sheet generation code
that relies heavily on an Apache library (POI): the
difference between Java’s setters and Clojure’s corresponding system for
specifying attributes. I had some boilerplate code littered with doto
and
(.setX Y Z)
statements:
(defn cell-style [workbook attrs]
(let [^CellStyle style (.createCellStyle workbook)
font (doto (.createFont workbook)
(.setBold (get attrs :bold false))
(.setItalic (get attrs :italic false))
(.setUnderline (get attrs :underline false))
(.setFontHeightInPoints (get attrs :font-height-in-points 11))
(.setFontName (get attrs :font-name "Calibri")))
;; ditto for objects for DataFormat, Alignment, Border, etc
]
(doto style
(.setFont font)
;; ditto for objects for DataFormat, Alignment, Border, etc
)))
Aside from being inherently boring, boilerplate is not necessary—or is at least
greatly reducible—in Lisp dialects! I can imagine a setters
macro that takes care of all of this:
(macroexpand
'(setters
{:bold true :italic true
:font-name "Arial" :font-height-in-points 12}))
; =>
; (fn*
; ([obj__1251__auto__]
; (clojure.core/doto obj__1251__auto__
; (.setBold true)
; (.setItalic true)
; (.setFontName "Arial")
; (.setFontHeightInPoints 12))))
(defmacro setters
"Given a compile-time literal map of attributes and values, return a function
that calls the corresponding setters on some java object.
E.g. (macroexpand
'(setters
{:bold true, :font-height-in-points 14, :font [\"large\" \"red\"]}))
; => (fn [obj]
(doto obj
(.setBold true)
(.setFontHeightInPoints 14)
(.setFont \"large\" \"red\")))"
[attrs]
(when-not (map? attrs)
(throw (ex-info "attrs must be a literal map, not a symbol" {})))
(let [capitalize (fn [coll] (map string/capitalize coll))
camel-case (fn [kw] (-> (name kw) (string/split #"\W") capitalize string/join))
setter-sym (fn [kw] (->> (camel-case kw) (str ".set") symbol))
expanded (map (fn [[a val]]
(if (vector? val)
`( ~(setter-sym a) ~@val)
`( ~(setter-sym a) ~val)))
attrs)]
`(fn [obj#] (doto obj# ~@expanded))))
Shout out to memfn
in clojure.core
, which clojureizes a Java function and
was the first thing I looked at when figuring out how to generate the setter code.
;;; clojure.core line 3837
(defmacro memfn
"Expands into code that creates a fn that expects to be passed an
object and any args and calls the named instance method on the
object passing the args. Use when you want to treat a Java method as
a first-class fn. name may be type-hinted with the method receiver's
type in order to avoid reflective calls."
{:added "1.0"}
[name & args]
(let [t (with-meta (gensym "target")
(meta name))]
`(fn [~t ~@args]
(. ~t (~name ~@args)))))
The excel generation code is now on github at excel-clj. You can see the setter-avoiding code in acction (and build a styled excel spreadsheet) with the following snippet:
(require '[excel-clj.core :as excel])
(def table-data
[{"Date" "2018-01-01" "% Return" 0.05M "USD" 1500.5005M}
{"Date" "2018-02-01" "% Return" 0.04M "USD" 1300.20M}
{"Date" "2018-03-01" "% Return" 0.07M "USD" 2100.66666666M}])
(letfn [(highlight-below-5% [row-data col-name]
(when (< (row-data "% Return") 0.05M)
{:fill-pattern :solid-foreground
:fill-foreground-color :yellow}))]
(excel/quick-open
{"My Generated Sheet" (excel/table table-data :data-style highlight-below-5%)}))
Which produces