Specifying default slot values for defrecord classes in Clojure

Clojure's datatypes are mana in a variety of ways.  In particular, for application-level programming, defrecord is the near-ideal construct – baked-in protocol support, Java interface interop, direct field storage of slots, fast keyword "accessors", fast-as-Java method implementations…the list of goodness goes on and on.

Of course, there's always room for improvement.  One big pain point for me was that I have a couple of records that, due to the domain, have a lot of slots, each of which needs to have a specific default value.  defrecord's generated constructor doesn't provide for any kind of default value for slots.  The naive solution would be to write a factory function that aligned its arguments with some defined set of defaults, but that sounded painful from the start: slot definitions are then functionally spread over multiple forms and the handling of arguments into that factory function would either be a maintenance or performance nightmare.

It's good to avoid macros as much as you can, but cases like this is where they shine:

[sourcecode language="clojure"] (defmacro defrecord+defaults "Defines a new record, along with a new-RecordName factory function that returns an instance of the record initialized with the default values provided as part of the record's slot declarations. e.g. (defrecord+ Foo [a 5 b \"hi\"]) (new-Foo) => #user.Foo{:a 5, :b \"hi\"}" [name slots & etc] (let [fields (->> slots (partition 2) (map first) vec) defaults (->> slots (partition 2) (map second))] `(do (defrecord ~name ~fields ~@etc) (defn ~(symbol (str "new-" name)) ~(str "A factory function returning a new instance of " name " initialized with the defaults specified in the corresponding defrecord+ form.") [] (~(symbol (str name .)) ~@defaults)) ~name))) [/sourcecode]

This macro is breaking my general rules of thumb that no def form should define more than one entity (here, a class and a factory function), and that def forms shouldn't create vars with generated names.  Unfortunately, I don't think there's any other better approach to be had given what I'm aiming to do.

There's an example in the docstring for defrecord+defaults, but let's take a look at some possible usage in a REPL interaction:

[sourcecode]user=> (defrecord+defaults SomeClass [size 0 root nil color java.awt.Color/black]) user.SomeClass user=> (doc new-SomeClass) ————————- user/new-SomeClass ([]) A factory function returning a new instance of SomeClass initialized with the defaults specified in the corresponding defrecord+ form. nil user=> (new-SomeClass)

user=> (assoc (new-SomeClass) :size 1 :root "foo") {:size 1, :root "foo", :color #<Color java.awt.Color[r=0,g=0,b=0]>}[/sourcecode]

Yup, macros to the rescue (yet again!). What's particularly handy about this is that the defined defaults can be any Clojure expression, which is evaluated each time the factory function is invoked. A neat future enhancement would be for the factory function to have a keyword-argument override that takes slot names and values that supersede the corresponding defaults provided in the defrecord specification.

I wouldn't be surprised if something like this ends up in Clojure itself eventually, as a specialization of a future defrecord factory function facility.