Search code examples
clojurerecord

Do records guarantee order when seq'd?


I'm writing some code where I need to map format over each value of a record. To save myself some duplicate writing, it would be super handy if I could rely on records having a set order. This is basically what it looks like right now:

(defrecord Pet [health max-health satiety max-satiety])

(let [{:keys [health max-health satiety max-satiety]} pet
        [h mh s ms] (mapv #(format "%.3f" (double %))
                          [health max-health satiety max-satiety])]
  ...)

Ideally, I'd like to write this using vals:

(let [[h mh s ms] (mapv #(format "%.3f" (double %)) (vals pet))]
  ...)

But I can't find any definitive sources on if records have a guaranteed ordering when seq'd. From my testing, they seem to be ordered. I tried creating a massive record (in case records rely on a sorted collection when small):

(defrecord Order-Test [a b c d e f g h i j k l m n o p q r s t u v w x y z
                       aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz])

(vals (apply ->Order-Test (range 52)))
=> (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51)

And they do seem to maintain order.

Can anyone verify this?


For this exact scenario, I supposed I could have reduce-kv'd over the record and reassociated the vals, then deconstructed. That would have gotten pretty bulky though. I'm also curious now since I wasn't able to find anything.


Solution

  • As with many things in Clojure, there is no guarantee because there is no spec. If it's not in the docstring for records, you assume it at your own risk, even if it happens to be true in the current version of Clojure.

    But I'd also say: that's not really what records mean, philosophically. Record fields are supposed to have individual domain semantics, and it looks like in your record they indeed do. It is a big surprise when an operation like "take the N distinctly meaningful fields of this record, and treat them all uniformly" is the right thing to do, and it deserves to be spelled out when you do it.

    You can at least do what you want with a bit less redundancy:

    (let [[h mh s ms] (for [k [:health :max-health, :satiety :max-satiety]]
                        (format "%.3f" (get pet k)))]
      ...)
    

    Personally I would say that you are modeling your domain wrong: you clearly have a concept of a "resource" (health and satiety) which has both a "current" and a "max" value. Those deserve to be grouped together by resource, e.g.

    {:health {:current 50 :max 80}
     :satiety {:current 3 :max 10}}
    

    and having done that, I'd say that a pet's "set of resources" is really just a single map field, rather than N fields for the N resources it contains. Then this whole question of ordering of record fields doesn't come up at all.