Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Clojure, how to group elements?

In clojure, I want to aggregate this data:

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

My problem is that :evening and :morning are redundant. Instead, I would like to create the following collection:

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

I came up with:

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

Is there a more idiomatic solution?

like image 665
viebel Avatar asked Feb 22 '12 10:02

viebel


2 Answers

I've come across similar grouping problems. Usually I end up plugging merge-with or update-in into some seq processing step:

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

You get a map, but this is just a seq of key-value pairs:

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

This solution only gets what you want if each key appears twice, however. This might be better:

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

Both of these feel "more functional" but also feel a little convoluted. Don't be too quick to dismiss your solution, it's easy-to-understand and functional enough.

like image 193
Mike Thvedt Avatar answered Oct 05 '22 04:10

Mike Thvedt


Don't be too quick to dismiss group-by, it has aggregated your data by the desired key and it hasn't changed the data. Any other function expecting a sequence of moment-fruit pairs will accept any value looked up in the map returned by group-by.

In terms of computing the summary my inclination was to reach for merge-with but for that I had to transform the input data into a sequence of maps and construct a "base-map" with the required keys and empty-vectors as values.

(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]}
like image 31
Alex Stoddard Avatar answered Oct 05 '22 03:10

Alex Stoddard