Can I map destructure with default values for present but `nil` keys?

(let [m {:foo nil}
      {:keys [foo]
       :or {foo "default"}} m]
  foo)
;; => nil
;; I want this to be "default". Is there a way to do this with destructuring?

No, because the key is present.

Since nil values are there in the sense that destructuring cares about, looks like I need to handle them manually:

(let [d {:foo "bar"}
      map-with-defaults (fn [m defaults]
			  (into {} (map (fn [[k v]] [k (or v (k defaults))])
					m)))]
  (map-with-defaults {:foo nil :not-foo nil} d))
;;=> {:foo "bar" :not-foo nil}

Actually, I see that (defaults k) would be better than (k defaults) since it would handle non-keyword identifiers

I would reach for reduce-kv here instead of into {} map. Also, using or could be surprising if your hash map contained Boolean values – false would get mapped to nil for keys you don’t care about defaulting.

Taking a step back, I would question why you started off with nil values in a hash map – it’s better to have the hash map simply not contain a key than for it to be present but with a nil value, in general.

Solving the Boolean problem:

(reduce-kv (fn [m k v] 
             (if (some? (get m k)) m (assoc m k v))) 
           d defaults)

This does fewer iterations (one per default, not one per original map entry) and won’t rebuild the map at all if all your defaulted keys are present with non-nil values.

2 Likes

reduce-kv is new to me! Awesome!

The reason that nil values will appear is because my map is essentially operating as a record: by virtue of its creation process, it will always have five keys even if some are empty.

Does not this work for you?

(let [m {:foo nil}
      {:keys [foo]} m]
  (or foo "default"))

When working with records you can do something like

(defrecord Foo [foo bar])

(defn init-foo [m]
  (map->Foo (merge {:foo "default"} m)))

(init-foo {:bar "bar"})

Thanks! The merge idiom is probably the right one. For reasons of superstition and irrationality, I’ve had a panic-reflex about merge ever since I watched https://youtu.be/3SSHjKT3ZmA

If the map being merged in has :foo nil then you will not get the defaulted value, you’ll get nil, which is what @Webdev_Tory is trying to avoid.

That’s one of the downside of records. Can you not use maps instead? Records are needed for polymorphic dispatch, like if you need to implement protocols, or for very rare scenarios where you need really fast access to specific keys.

If you just want to define entity schemas, better to use spec and a constructor function for it like:

(s/def ::name (complement str/blank?))
(s/def ::age (s/int-in 1 150))
(s/def ::height (s/int-in 0 300))
(s/def ::person
  (s/keys :req [::name ::age]
          :opt [::height]))

(defn make-person [person]
  (if (s/valid? ::person person)
    person
    (throw (ex-info (s/explain-str ::person person) (s/explain-data ::person person)))))
2 Likes

That’s a cool example; do you really inline your spec beside the action functions like that?

I may redesign this application to be less record-y.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.