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