问题
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