В Clojure, как группировать элементы?

В clojure я хочу агрегировать эти данные:

(def data [[:morning :pear][:morning :mango][:evening :mango][:evening :pear]])
(group-by first data)
;{:morning [[:morning :pear][:morning :mango]],:evening [[:evening :mango][:evening :pear]]}

Моя проблема в том, что :evening и :morning являются избыточными. Вместо этого я хотел бы создать следующую коллекцию:

([:morning (:pear :mango)] [:evening (:mango :pear)])

Я придумал:

(for [[moment moment-fruit-vec] (group-by first data)] [moment (map second moment-fruit-vec)])

Есть ли более идиоматическое решение?

Ответ 1

Я столкнулся с подобными проблемами группировки. Обычно я заканчиваю подключением слияния или обновления на некоторый шаг обработки seq:

(apply merge-with list (map (partial apply hash-map) data))

Вы получаете карту, но это всего лишь пара пар ключ-значение:

user> (apply merge-with list (map (partial apply hash-map) data))
{:morning (:pear :mango), :evening (:mango :pear)}
user> (seq *1)
([:morning (:pear :mango)] [:evening (:mango :pear)])

Это решение получает только то, что вы хотите, если каждый ключ появляется дважды. Это может быть лучше:

(reduce (fn [map [x y]] (update-in map [x] #(cons y %))) {} data)

Оба они чувствуют себя "более функциональными", но также чувствуют себя немного запутанными. Не будьте слишком быстры, чтобы отклонить свое решение, это легко понять и достаточно функционально.

Ответ 2

Не слишком быстро отклоните group-by, он агрегировал ваши данные по желаемому ключу и не изменил данные. Любая другая функция, ожидающая последовательность пар "момент-фрукты", примет любое значение, полученное на карте, возвращаемой group-by.

В терминах вычисления сводки моя склонность заключалась в достижении merge-with, но для этого мне пришлось преобразовать входные данные в последовательность карт и построить "базовую карту" с необходимыми ключами и пустыми векторами в качестве значения.

(let [i-maps (for [[moment fruit] data] {moment fruit})
      base-map (into {} 
                  (for [key (into #{} (map first data))] 
                    [key []]))]
      (apply merge-with conj base-map i-maps))

{:morning [:pear :mango], :evening [:mango :pear]}

Ответ 3

Размышляя над @mike t, я придумал:

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

Это решение работает также, когда клавиши появляются более чем в два раза на data:

 (apply merge-with agg (map (partial apply hash-map) 
     [[:morning :pear][:morning :mango][:evening :mango] [:evening :pear] [:evening :kiwi]]))
;{:morning (:mango :pear), :evening (:kiwi :pear :mango)}

Ответ 4

может быть, просто немного изменим стандартную группу:

(defn my-group-by 
  [fk fv coll]  
  (persistent!
   (reduce
    (fn [ret x]
      (let [k (fk x)]
        (assoc! ret k (conj (get ret k []) (fv x)))))
    (transient {}) coll)))

затем используйте его как:

(my-group-by first second data)