12
votes

I'm having trouble finding solution to the following problem:

Lets say I have a map:

(def defaults {
  :name    "John"
  :surname "Doe"
  :info    {:date-of-birth "01-01-1980"
            :registered    [{:type "newsletter" :name "breaking news" }]}
})

And then I pass a similar structured map but I want to conjoin the vectors and overwrite the rest of the keys:

(def new {
  :name    "Peter"
  :info    {:date-of-birth "11-01-1986"
            :registered    [{:type "alert" :name "mobile-alert" }]}
})

And I want this result:

 {:name    "Peter"
  :surname "Doe"
  :info    {:date-of-birth "11-01-1986"
            :registered    [{:type "newsletter" :name "breaking news" }
                            {:type "alert"      :name "mobile-alert" }]}}

Now I can do this easily by using static syntax like:

(reduce conj (get-in defaults [:info :registered]) (get-in new [:info :registered]))

(There is probably a better way...) But I was hoping more of a dynamic function with the following properties:

  1. Keep all keys from both maps, without knowing the structure
  2. Update any keys with the values from the right map
  3. if the val of a key is a vector, then conj the vector with the vector of the right map (if the appropriate key exists of course)

Thanks for the help in advance :)

2

2 Answers

25
votes

You should definitely look at merge-with function. This is possible implementation:

(defn deep-merge [a b]
  (merge-with (fn [x y]
                (cond (map? y) (deep-merge x y) 
                      (vector? y) (concat x y) 
                      :else y)) 
                 a b))
0
votes

Here's a possible implementation for this kind of functionality. It is at least a starting point, you may need some extra validations depending on the possible structure of your data (e.g. what if the overriding map's value is a vector but the value in the default map is not even a collection?).

(declare merge-maps)

(defn merge-map [x [k v]]
  (cond (vector? v)
          (assoc x k (vec (reduce conj (x k) v)))
        (map? v)
          (assoc x k (merge-maps (x k) v))
        :esle
          (assoc x k v)))

(defn merge-maps [x y]
  (reduce merge-map x y))

(merge-maps defaults new)

;= {:info {:date-of-birth "11-01-1986", 
;=         :registered [{:name "breaking news", :type "newsletter"} 
;=                      {:name "mobile-alert", :type "alert"}]}, 
;=  :name "Peter", 
;=  :surname "Doe"}