-
-
Save danielpcox/c70a8aa2c36766200a95 to your computer and use it in GitHub Desktop.
(ns deep-merge-spec | |
(:require [midje.sweet :refer :all] | |
[util :as u])) | |
(fact (u/deep-merge {:one 1 :two 2} | |
{:one 1 :two {}}) | |
=> {:one 1 :two {}}) | |
(fact (u/deep-merge {:one 1 :two {:three 3 :four {:five 5}}} | |
{:two {:three {:test true}}}) | |
=> {:one 1 :two {:three {:test true} :four {:five 5}}}) | |
(fact (u/deep-merge {:one {:two {:three 3}} | |
:four {:five {:six 6}}} | |
{:one {:seven 7 :two {:three "three" :nine 9}} | |
:four {:eight 8 :five 5} | |
:ten 10}) | |
=> {:one {:two {:three "three" :nine 9} | |
:seven 7} | |
:four {:five 5 :eight 8} | |
:ten 10}) | |
(fact (u/deep-merge {:one {:two 2 :three 3}} | |
{:one {:four 4 :five 5}}) | |
=> {:one {:two 2 :three 3 :four 4 :five 5}}) | |
(fact (u/deep-merge {:one 1 :two {:three 3}} | |
{:one 2 :two {:three 4}} | |
{:one 3 :two {:three 5}} | |
{:one 4 :two {:three 6}}) | |
=> {:one 4 :two {:three 6}}) | |
(ns util) | |
(defn deep-merge [v & vs] | |
(letfn [(rec-merge [v1 v2] | |
(if (and (map? v1) (map? v2)) | |
(merge-with deep-merge v1 v2) | |
v2))] | |
(when (some identity vs) | |
(reduce #(rec-merge %1 %2) v vs)))) |
Note that the second, variadic version of deep-merge
provided here has unusual/incorrect behavior for false-y values. For example:
dev=> (deep-merge {:a nil} {:a false})
{:a nil}
This is due to merge-with
checking for at least one "truthy" argument:
(defn merge-with
"Returns a map ..."
[f & maps]
(when (some identity maps)
(let [merge-entry (fn [m e]
...
(reduce1 merge2 maps))))
Which produces behavior like:
dev=> (merge-with first false nil)
nil
After using deep-merge
for a bit, I think I know why it doesn't already exist. It encourages difficult to understand nested maps when flat maps are simpler/better. Perhaps its absence speaks to it as a code smell.
@fl00r Here's the problem (if that makes sense still):
cljs.user=> (defn deep-merge [& maps]
#_=> (apply merge-with (fn [& args]
#_=> (if (every? map? args)
#_=> (apply deep-merge args)
#_=> (last args)))
#_=> maps))
#_=>
#'cljs.user/deep-merge
cljs.user=> (deep-merge {:m {:a 1 :b 2}} {:m nil} {:m {:a 2 :c 3}})
{:m {:a 2, :c 3}}
cljs.user=> (deep-merge {:m {:a 1 :b 2}} {:m {}} {:m {:a 2 :c 3}})
{:m {:a 2, :b 2, :c 3}}
So I guess sth like this:
(defn deep-merge [& maps]
(apply merge-with (fn [& args]
(if (every? #(or (map? %) (nil? %)) args)
(apply deep-merge args)
(last args)))
maps))
@andrewboltachev I would argue that this behavior is an expected one.
nil
is a legit value, so (deep-merge {:m {:a 1 :b 2}} {:m nil})
=> {:m nil}
cheers
@fl00r well, my argument here is that, when using just merge
:
user=> (merge {:a 1 :b 2} nil)
{:a 1, :b 2}
and I think of nil
as kinda "absence of value"
but
(merge {:a 1} {:a nil})
{:a nil}
In deep merge I would prefer this semantic.
But that is of course a matter of taste.
And as @dijonkitchen mentioned
After using deep-merge for a bit, I think I know why it doesn't already exist. It encourages difficult to understand nested maps when flat maps are simpler/better. Perhaps its absence speaks to it as a code smell.
Of course you can modify the code to fit your needs.
Just for some shameless self promotion:
https://github.com/JasonStiefel/clojure-deep-merge/blob/master/src/deep/merge.clj
Here's one more version.
This one is a lot less concise, but potentially easier to understand. It might also perform better than some other versions, since I'm using loops and recurs.
(defn merge-entries [value-fn into-map entries]
(loop [result into-map
remaining (seq entries)]
(if-not remaining
result
(let [entry (first remaining)
k (key entry)
new-val (val entry)
cur-val (get result k)]
(recur (if (nil? cur-val)
(assoc result k new-val)
(assoc result k (value-fn cur-val new-val)))
(next remaining))))))
(defn merge-maps [value-fn & maps]
(loop [results nil
remaining (seq maps)]
(if-not remaining
results
(recur (merge-entries value-fn (or results {}) (-> remaining first seq))
(next remaining)))))
(defn deep-merge [& maps]
(let [value-fn (fn [v1 v2]
(if-not (and (map? v1) (map? v2))
v2
(deep-merge v1 v2)))]
(apply merge-maps value-fn maps)))
(deep-merge {:m {:a 1 :b 2}} {:m nil} {:m {:a 2 :c 3}})
(deep-merge {:m {:a 1 :b 2}} {:m {}} {:m {:a 2 :c 3}})
(deep-merge {:a {:b true}} {:a {:b false}} {:a {:b nil}})
(deep-merge {:a 1} nil)
(deep-merge {:a {:b true}} {:a {:b false}} {:a {:b nil}})
(deep-merge {:one 1 :two {:three 3}}
{:one 2 :two {:three 4}}
{:one 3 :two {:three 5}}
{:one 4 :two {:three 6}})
{:m {:a 2, :c 3}}
{:m {:a 2, :b 2, :c 3}}
{:a {:b nil}}
{:a 1}
{:a {:b nil}}
{:one 4, :two {:three 6}}
And @jaihindhreddy
and variadic: