Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure: Assigning defrecord fields from Map

Following up on How to make a record from a sequence of values, how can you write a defrecord constructor call and assign the fields from a Map, leaving un-named fields nil?

(defrecord MyRecord [f1 f2 f3])
(assign-from-map MyRecord {:f1 "Huey" :f2 "Dewey"})  ; returns a new MyRecord

I imagine a macro could be written to do this.

like image 730
Ralph Avatar asked Dec 23 '10 15:12

Ralph


3 Answers

You can simply merge the map into a record initialised with nils:

(merge (MyRecord. nil nil nil) {:f1 "Huey" :f2 "Dewey"})

Note that records are capable of holding values stored under extra keys in a map-like fashion.

The list of a record's fields can be obtained using reflection:

(defn static? [field]
  (java.lang.reflect.Modifier/isStatic
   (.getModifiers field)))

(defn get-record-field-names [record]
  (->> record
       .getDeclaredFields
       (remove static?)
       (map #(.getName %))
       (remove #{"__meta" "__extmap"})))

The latter function returns a seq of strings:

user> (get-record-field-names MyRecord)
("f1" "f2" "f3")

__meta and __extmap are the fields used by Clojure records to hold metadata and to support the map functionality, respectively.

You could write something like

(defmacro empty-record [record]
  (let [klass (Class/forName (name record))
        field-count (count (get-record-field-names klass))]
    `(new ~klass ~@(repeat field-count nil))))

and use it to create empty instances of record classes like so:

user> (empty-record user.MyRecord)
#:user.MyRecord{:f1 nil, :f2 nil, :f3 nil}

The fully qualified name is essential here. It's going to work as long as the record class has been declared by the time any empty-record forms referring to it are compiled.

If empty-record was written as a function instead, one could have it expect an actual class as an argument (avoiding the "fully qualified" problem -- you could name your class in whichever way is convenient in a given context), though at the cost of doing the reflection at runtime.

like image 54
Michał Marczyk Avatar answered Nov 17 '22 17:11

Michał Marczyk


Clojure generates these days a map->RecordType function when a record is defined.

(defrecord Person [first-name last-name])
(def p1 (map->Person {:first-name "Rich" :last-name "Hickey"}))

The map is not required to define all fields in the record definition, in which case missing keys have a nil value in the result. The map is also allowed to contain extra fields that aren't part of the record definition.

like image 5
Qrt Avatar answered Nov 17 '22 19:11

Qrt


As mentioned in the linked question responses, the code here shows how to create a defrecord2 macro to generate a constructor function that takes a map, as demonstrated here. Specifically of interest is the make-record-constructor macro.

like image 1
Alex Miller Avatar answered Nov 17 '22 19:11

Alex Miller