Return plain map from a Clojure record
Asked Answered
A

3

5

I have a record:

(defrecord Point [x y])
(def p (Point. 1 2))

Now I want to extract just the map from the record. These ways get the job done. Are these good ways? Are there better ways?

(into {} (concat p))
(into {} (map identity p))
(apply hash-map (apply concat p))

I was hoping there might be a cleaner way, perhaps built-in to the notion of a record.

Americana answered 27/7, 2014 at 4:8 Comment(3)
Why do you need to do this? A record is an implementation of a persistent map already.Placement
Yes, I know that a record can do all of the things a map can do. I still want to do it. Ideas?Americana
That is better and a good answer (nudge)! I just looked through APersisentMap.java but didn't find a built-in Java method.Americana
P
8

Records are maps

(defrecord Thing [a b])

(def t1 (Thing. 1 2))
(instance? clojure.lang.IPersistentMap t1) ;=> true

So, in general there is no need to coerce them into a APersistentMap type. But, if desired you do so with into:

(into {} t1) ;=> {:a 1, :b 2}

If you want to traverse an arbitrary data structure, including nested records, making this transformation, then use walk

(def t2 (Thing. 3 4))
(def t3 (Thing. t1 t2))
(def coll (list t1 [t2 {:foo t3}]))

(clojure.walk/postwalk #(if (record? %) (into {} %) %) coll)
;=> ({:a 1, :b 2} [{:a 3, :b 4} {:foo {:a {:a 1, :b 2}, :b {:a 3, :b 4}}}])
Placement answered 28/7, 2014 at 15:12 Comment(2)
Thanks! My use case was to 'mapify' records inside a data structure to serialize in a compact way to make tests more readable. (Clojure's default record serialization is very verbose, especially if you have type hints and use print-dup.)Americana
@DavidJames ((get-method print-method java.util.Map) (Thing. 1 2) *out*) ;=> {:a 1, :b 2}Placement
A
1

A. Webb suggested the much-simpler (into {} p) in the comments. Thanks!

Here is a code snippet that is more general; it works for recursive records:

(defrecord Thing [a b])
(def t1 (Thing. 1 2))
(def t2 (Thing. 3 4))
(def t3 (Thing. t1 t2))

(defn record->map
  [record]
  (let [f #(if (record? %) (record->map %) %)
        ks (keys record)
        vs (map f (vals record))]
    (zipmap ks vs)))

(record->map t3)
; {:b {:b 4, :a 3}, :a {:b 2, :a 1}}
Americana answered 27/7, 2014 at 4:41 Comment(2)
(clojure.walk/postwalk #(if (record? %) (into {} %) %) t3)Placement
A. Webb: That's a better answer. Want to post it?Americana
A
0

I also wrote a general function that converts records to maps for (most) arbitrary Clojure data structures:

(defn derecordize
  "Returns a data structure equal (using clojure.core/=) to the
  original value with all records converted to plain maps."
  [v]
  (cond
    (record? v) (derecordize (into {} v))
    (list? v) (map derecordize v)
    (vector? v) (mapv derecordize v)
    (set? v) (set (map derecordize v))
    (map? v) (zipmap (map derecordize (keys v)) (map derecordize (vals v)))
    :else v))

I know this is unusual. This is useful when you need to export a data structure without records.

Americana answered 28/7, 2014 at 14:41 Comment(2)
This is what walk does. It understands arbitrary data structures, lists, vectors, maps, sets ... including records.Placement
Also you should basically never call list? - you mean to use seq? instead in this case (as in most cases). Your proposed solution would not work for, for example, (for [x (range 5)] (MyRecord. x)).Dead

© 2022 - 2024 — McMap. All rights reserved.