defrecord and deftype improvements for Clojure 1.3

Motivation

The Java unification of records prevents them from being first class, in either the data or fn sense:

Solutions

For the sake of discussion, focus will revolve around example defrecords and deftypes defined as

(ns myns)

(defrecord MyRecord [a b])
(deftype MyType [a b])

Semantics of records as first class data

The semantics of record reader forms and record factory functions are defined as follows:

(-> (MyRecord. <initialization value> <initialization value>)
    (into {:a 1, :b 2})
    <validation>)

note: The semantics illustrated above should not be taken as implementation detail. at the moment <validation> is undefined and should be considered a no-op.

The <initialization value> refers to the same default default values for Java primitive types (as defined by type hinting on the record fields) or nil for instances. For record reader forms, the keys and values must remain as constants as their semantics require that the readable form coincide with the evalable form.

Record and Type reader forms

There would be two additional reader forms added to Clojure.

Labelled record reader form
#myns.MyRecord{:a 1, :b 2}
Positional record and type reader forms
#myns.MyRecord[1 2]

and

#myns.MyType[1 2]

This syntax satisfies the need for a general-purpose Java class construction reader form. However, not all Java classes are considered fully constructed after the use of their constructors. Therefore, serialization support is not provided for any Java classes by default. For instances such as these, Clojure will continue to provide facilities via print-dup in the known ways.

Generated factory functions

When defining a new defrecord, two functions will also be defined in the same namespace as the record itself. For new deftypes, only the positional constructor outlined below is generated.

Factory function taking a map (defrecord only)

A factory function named map->MyRecord taking a map is defined by defrecord.

(myns/map->MyRecord {:a 1, :b 2})

;=> #myns.MyRecord{:a 1, :b 2}
Factory function taking positional values (defrecord and deftype)

A factory function named ->MyRecord taking positional values (as defined by the record ctor) is also defined by defrecord.

(myns/->MyRecord 1 2)

;=> #myns.MyRecord{:a 1, :b 2}

and

(myns/->MyType 1 2)

;=> #<MyType myns.MyType@2ed277f2>

Writing records

When writing record data for the purposes of serialization, the positional reader form is used by default:

(binding [*print-dup* true]
  (pr-str (MyRecord. 1 2)))

;=> "#myns.MyRecord[1, 2]"

However, if you wish to use the map reader form instead, then the following would work:

(binding [*print-dup* true
          *verbose-defrecords* true]
  (pr-str (MyRecord. 1 2)))

;=> "#myns.MyRecord{:a 1, :b 2}"

note: printing forms for types are not provided by default

Tool support

Defining Clojure defrecords will also expose static class methods useable at the Java API level. These methods are not documented with the intention of public consumption and are considered implementation details.

Static factory for defrecords

The static factory exposed will mirror the map->MyRecord function:

(MyRecord/create aMap)
Basis access

A static factory allowing access to the basis keys will also be provided:

(MyRecord/getBasis)
;=> [a b]

and

(MyType/getBasis)
;=> [a b]

The getBasis method will return a PersistentVector of Symbols with (potentially) attached metadata for each field.

Old Ideas

Lesser Problems:

Challenges:

Some Options:

Tentative Proposal 1:

Define a k/v syntax for read and write that does not require a factory fn.

Tentative Proposal 2:

Autogenerate a k/v factory fn for all defrecords.

Some history:

The record multimethod was almost ready to go when Rich raised the GC issue. What happens when somebody creates a ton of record classes over time? GC can collect records that are not longer in use, but doesn't clean up the old multimethod functions.

Additional Reading

Some (non-contributed) code that demonstrates people's need for this: