In clojure, how to merge several maps combining mappings with same key into a list?

南笙酒味 提交于 2019-12-21 06:58:21

问题


In Clojure, I would like to combine several maps into a single map where mappings with same key are combined into a list.

For example:

{:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny}

should lead to:

{:weather :sunny, :humor (:happy :sad :happy)}

I thought about:

(merge-with (comp flatten list) data)

But it is not efficient because flatten has O(n) complexity.

Then I came up with:

(defn agg[x y] (if (coll? x) (cons y x) (list y x)))
(merge-with agg data)

But it feels not idiomatic. Any other idea?


回答1:


One approach would be

(defn merge-lists [& maps]
  (reduce (fn [m1 m2]
            (reduce (fn [m [k v]]
                      (update-in m [k] (fnil conj []) v))
                    m1, m2))
          {}
          maps))

It's a bit ugly, but that's only because your values aren't already lists. It also forces everything to be a list (so you'd get :weather [:sunny] rather than :weather :sunny). Frankly, this is likely to be a million times easier for you to work with anyway.

If you had each value as a vector already, you could simply do (apply merge-with into maps).




回答2:


@amalloy's answer can flattened a little bit by using for.

(reduce (fn [m [k v]] (update-in m [k] (fnil conj []) v)) {} (for [m data entry m] entry))

Source for this technique: http://clj-me.cgrand.net/2010/01/19/clojure-refactoring-flattening-reduces/




回答3:


You could try the following, I think it's pretty efficient

(reduce 
  (fn [m pair] (let [[[k v]] (seq pair)]
                 (assoc m k (cons v (m k))))) 
  {} 
  data)

=> {:weather (:sunny), :humor (:happy :sad :happy)}



回答4:


Merge with this function:

(defn acc-list [x y]
  (let [xs (if (seq? x) x (cons x nil))]
    (cons y xs)))



回答5:


What about using group-by? It doesn't return exactly what you ask for but it is very similar:

user=> (group-by first (concat {:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny :humor :whooot}))
{:humor [[:humor :happy] [:humor :sad] [:humor :happy] [:humor :whooot]], :weather [[:weather :sunny]]}

Or with a small modification to the group-by function:

(defn group-by-v2
 [f vf coll]
  (persistent!
    (reduce
     (fn [ret x]
       (let [k (f x)]
         (assoc! ret k (conj (get ret k []) (vf x)))))
     (transient {}) coll)))

becomes:

user=> (group-by-v2 key val (concat {:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny :humor :whooot}))
{:humor [:happy :sad :happy :whooot], :weather [:sunny]}



回答6:


Here's a solution where every value is represented as lists, even if singletons:

(->> [{:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny}]
     (map first)
     (reduce (fn [m [k v]] (update-in m [k] #(cons v %))) {}))

=> {:weather (:sunny), :humor (:happy :sad :happy)}

If you don't want to wrap singletons in a list then I thought your original solution was just fine. The only way to make it more idiomatic is to use core.match.

(->> [{:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny}]
     (apply merge-with #(match %1
                               [& _] (conj %1 %2)
                               :else [%1 %2])))

=> {:weather :sunny, :humor [:happy :sad :happy]}


来源:https://stackoverflow.com/questions/9408846/in-clojure-how-to-merge-several-maps-combining-mappings-with-same-key-into-a-li

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!