Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Merge list of maps and combine values to sets in Clojure

What function can I put as FOO here to yield true at the end? I played with hash-set (only correct for first 2 values), conj, and concat but I know I'm not handling the single-element vs set condition properly with just any of those.

(defn mergeMatches [propertyMapList]
    "Take a list of maps and merges them combining values into a set"
    (reduce #(merge-with FOO %1 %2) {} propertyMapList))

(def in 
    (list
        {:a 1}
        {:a 2}
        {:a 3}
        {:b 4}
        {:b 5}
        {:b 6} ))

(def out
    { :a #{ 1 2 3}
      :b #{ 4 5 6} })

; this should return true
(= (mergeMatches in) out)

What is the most idiomatic way to handle this?

like image 269
Alex Miller Avatar asked Feb 04 '10 21:02

Alex Miller


3 Answers

This'll do:

(let [set #(if (set? %) % #{%})]
  #(clojure.set/union (set %) (set %2)))

Rewritten more directly for the example (Alex):

(defn to-set [s]
    (if (set? s) s #{s}))
(defn set-union [s1 s2] 
    (clojure.set/union (to-set s1) (to-set s2)))
(defn mergeMatches [propertyMapList]
    (reduce #(merge-with set-union %1 %2) {} propertyMapList))
like image 105
cemerick Avatar answered Nov 10 '22 09:11

cemerick


Another solution contributed by @wmacgyver on Twitter based on multimaps:

(defn add
  "Adds key-value pairs the multimap."
  ([mm k v]
     (assoc mm k (conj (get mm k #{}) v)))
  ([mm k v & kvs]
     (apply add (add mm k v) kvs)))
(defn mm-merge
  "Merges the multimaps, taking the union of values."
  [& mms]
  (apply (partial merge-with union) mms))   

(defn mergeMatches [property-map-list]
  (reduce mm-merge (map #(add {} (key (first %)) (val (first %))) property-map-list)))      
like image 39
Alex Miller Avatar answered Nov 10 '22 09:11

Alex Miller


I wouldn't use merge-with for this,

(defn fnil [f not-found]
  (fn [x y] (f (if (nil? x) not-found x) y)))
(defn conj-in [m map-entry]
  (update-in m [(key map-entry)] (fnil conj #{}) (val map-entry)))
(defn merge-matches [property-map-list]
  (reduce conj-in {} (apply concat property-map-list)))

user=> (merge-matches in)
{:b #{4 5 6}, :a #{1 2 3}}

fnil will be part of core soon so you can ignore the implementation... but it just creates a version of another function that can handle nil arguments. In this case conj will substitute #{} for nil.

So the reduction conjoining to a set for every key/value in the list of maps supplied.

like image 4
Timothy Pratley Avatar answered Nov 10 '22 10:11

Timothy Pratley