(david-mcneil.com :blog)

2011 April 6 7:37pm

In the course of doing Clojure development I have made extensive use of records and have extended them in many was. So I was excited to see a proposal from the Clojure Core team for defrecord improvements. It looks like a good start, below are my thoughts and questions about the proposal.

1) Variety of forms

One question that arises is how to understand the difference between the various forms. For example these two forms:

#myns.MyRecord[1 2]


(MyRecord. 1 2)

As best I can understand it, the first form would have the validation function applied but the second would not.

Furthermore as a literal form, the first can only include constants. So for instance the following use of a function call would not be valid in the first syntax:

(MyRecord. 1 (+ 1 1))

If you need to write expressions to compute field values and want the validation function to be applied then you use one of the factory functions:

(myns/->MyRecord 1 (+ 1 1))

I assume that the position forms will require all of the fields to be provided? If so, the initialization values will only be relevant to the map forms.

My understanding of the proposal is that the literal record syntax is a general syntax for Java objects. So the object created by

(java.util.Locale. "en" "US") 

could be expressed in the literal syntax as

#java.util.Locale["en" "US"]

However, it doesn’t seem to me to be generally possible to know which contructor to use for a given object so I am not sure when this literal syntax will be used for printing (non-record) Java objects.

2) Factory function naming

I realize that the names in the proposals are just placeholders… my preference is to not use “->” (which could easily be confused with the threading macro “->”) in the names and to name the functions with lower-case-dashed versions of the record names instead of CamelCase versions. For my sense of aesthetics this makes the use of record factory functions look like “normal” function calls. Furthermore, I value the ability to specify the name of the factory functions at the time the record is defined.

3) Validation

One of the forms of validation that we have found particularly helpful is to validate the names of the fields passed into the map factory function. If the map contains a key that is not a record field name then an exception is thrown. It is possible to add additional, non field-name keys, with assoc.

In addition I think it is useful to allow a validation function to be defined as part of the defrecord.

4) Writing records

I value an option to exclude the namespace from the printed from of the records. Instead of this:

(myns/->MyRecord 1 2)

They would optionally print as:

(->MyRecord 1 2)

This is useful when printing deeply nested trees of records because it trims a potentially long namespace identifier from every object’s output.

Finally, we have found it useful to suppress nil values when printing records with the map factory form. Again this makes the output less verbose.

5) Mutators

Beyond the initial creation of records we have found it useful to provide functions to create new record objects from existing objects. For example, the syntax could be:

(def x (myns/map->MyRecord {:a 1}))
(myns/map->MyRecord x {:b 2})
;; -> (myns/->MyRecord 1 2)

By virtue of going through the factory function the validations are applied. As opposed to using assoc directly in which case the validations are not applied.

Related to this is the idea of a universal constructor, e.g. named “new-record”:

(def x (myns/map->MyRecord {:a 1}))
(new-record x {:b 2})
;; -> (myns/->MyRecord 1 2)

This allows a new record object to be created from a record object without knowing the type of the object. We have found this useful for writing generic code to handle record objects.

Finally we have found it useful to define a dissoc function that removes a key from a record object, but produces a record object as the result.

So instead of this default behavior from Clojure:

(class (dissoc (map->MyRecord {:a 1 :b 2}) :b))
;; -> clojure.lang.PersistentArrayMap

We would get:

(class (dissoc2 (map->MyRecord {:a 1 :b 2}) :b))
;; -> myns.MyRecord

6) record?

We have found it useful to define a record? predicate function that reports whether a given object is a record object:

(record? (map->MyRecord {:a 1 :b 2}))
;; -> true
(record? "hello")
;; -> false

7) walking records

We have extended defrecord to define prewalk and postwalk support and this has proven useful (despite the fact that pre/postwalk are semi-deprecated).

8) zipper support

We have extended defrecord to generate multi-method implementations for each record class to participate in a zip-record function that allows zippers to be used to navigate record trees. We have used this feature extensively in our product to manipulate record trees.

9) matchure support

We have extended defrecord to support matchure. Specifically all records participate in a multi-method that allows them to be used with a “match-record” of our creation that delegates to matchure if-match . For example:

(match-record [(map->MyRecord {:a 1 :b ?b})
               (map->MyRecord {:a 1 :b 2})]
;; -> 2

past life