Подтвердить что ты не робот

В 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)

Но он не чувствует себя идиоматическим. Любая другая идея?

4b9b3361

Ответ 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).

Ответ 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]}