swizzard
swizzard

Reputation: 1095

clojure same ":or" value for all keys

I've defined a record with a bunch of fields--some of which are computed, some of which don't map directly to keys in the JSON data I'm ingesting. I'm writing a factory function for it, but I want to have sensible default/not-found values. Is there a better way that tacking on :or [field1 "" field2 "" field3 "" field4 ""...]? I could write a macro but I'd rather not if I don't have to.

Upvotes: 1

Views: 316

Answers (2)

Leon Grapenthin
Leon Grapenthin

Reputation: 9266

There are three common idioms for implementing defaults in constructor functions.

  1. :or destructoring

    Example:

    (defn make-creature [{:keys [type name], :or {type :human
                                                  name (str "unnamed-" (name type))}}]
      ;; ...
      )
    

    This is useful when you want to specify the defaults inline. As a bonus, it allows let style bindings in the :or map where the kvs are ordered according to the :keys vector.

  2. Merging

    Example:

    (def default-creature-spec {:type :human})
    
    (defn make-creature [spec]
       (let [spec (merge default-creature-spec
                         spec)]
          ;; ....
          ))
    

    This is useful when you want to define the defaults externally, generate them at runtime and/or reuse them elsewhere.

  3. Simple or

    Example:

    (defn make-creature [{:keys [type name]}]
      (let [type (or type :human)
            name (or name (str "unnamed-" (name type)))]
         ;; ...
         ))
    

    This is as useful as :or destructoring but only those defaults are evaluated that are actually needed, i. e. it should be used in cases where computing the default adds unwanted overhead. (I don't know why :or evaluates all defaults (as of Clojure 1.7), so this is a workaround).

Upvotes: 1

nberger
nberger

Reputation: 3659

If you really want the same default value for all the fields, and they really have to be different than nil, and you don't want to write them down again, then you can get the record fields by calling keys on an empty instance, and then construct a map with the default values merged with the actual values:

(defrecord MyFancyRecord [a b c d])

(def my-fancy-record-fields (keys (map->MyFancyRecord {})))
;=> (:a :b :c :d)

(def default-fancy-fields (zipmap my-fancy-record-fields (repeat "")))

(defn make-fancy-record [fields]
  (map->MyFancyRecord (merge default-fancy-fields
                             fields)))

(make-fancy-record {})
;=> {:a "", :b "", :c "", :d ""}

(make-fancy-record {:a 1})
;=> {:a 1, :b "", :c "", :d ""}

To get the list of record fields you could also use the static method getBasis on your record class:

(def my-fancy-record-fields (map keyword (MyFancyRecord/getBasis)))

(getBasis is not part of the public records api so there are no guarantees it won't be removed in future clojure versions. Right now it's available in both clojure and clojurescript, it's usage is explained in "Clojure programming by Chas Emerick, Brian Carper, Christophe Grand" and it's also mentioned in this thread during a discussion about how to get the keys from a record. So, it's up to you to decide if it's a good idea to use it)

Upvotes: 0

Related Questions