В clojure, как объединить несколько карт, объединяющих сопоставления с одним и тем же ключом в список?
В Clojure я хотел бы объединить несколько карт в одну карту, где сопоставления с одним ключом объединены в список.
Например:
{:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny}
должно привести к:
{:weather :sunny, :humor (:happy :sad :happy)}
Я думал о:
(merge-with (comp flatten list) data)
Но это не эффективно, потому что flatten имеет сложность O (n).
Затем я придумал:
(defn agg[x y] (if (coll? x) (cons y x) (list y x)))
(merge-with agg data)
Но он не чувствует себя идиоматическим. Любая другая идея?
Ответы
Ответ 1
Один подход -
(defn merge-lists [& maps]
(reduce (fn [m1 m2]
(reduce (fn [m [k v]]
(update-in m [k] (fnil conj []) v))
m1, m2))
{}
maps))
Это немного уродливо, но это только потому, что ваши значения уже не являются списками. Это также заставляет все быть списком (так что вы получите :weather [:sunny]
, а не :weather :sunny
). Честно говоря, это, вероятно, будет в миллион раз легче для вас работать в любом случае.
Если бы у вас было какое-то значение как вектор, вы могли бы просто сделать (apply merge-with into maps)
.
Ответ 2
Ответ @amalloy может немного сгладить, используя for
.
(reduce (fn [m [k v]] (update-in m [k] (fnil conj []) v)) {} (for [m data entry m] entry))
Источник для этой техники: http://clj-me.cgrand.net/2010/01/19/clojure-refactoring-flattening-reduces/
Ответ 3
Вы можете попробовать следующее, я думаю, что это довольно эффективно
(reduce
(fn [m pair] (let [[[k v]] (seq pair)]
(assoc m k (cons v (m k)))))
{}
data)
=> {:weather (:sunny), :humor (:happy :sad :happy)}
Ответ 4
Объединить с этой функцией:
(defn acc-list [x y]
(let [xs (if (seq? x) x (cons x nil))]
(cons y xs)))
Ответ 5
Как насчет использования группового? Он не возвращает точно, что вы просите, но он очень похож:
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]]}
Или с небольшой модификацией групповой функции:
(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)))
становится:
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
Здесь решение, в котором каждое значение представлено в виде списков, даже если одиночные числа:
(->> [{: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)}
Если вы не хотите обертывать синглтоны в списке, я подумал, что ваше оригинальное решение было в порядке. Единственный способ сделать его более идиоматичным - использовать 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]}