August 31, 2018

Just Juxt #17: Group a Sequence (4clojure #63)

Toons

Given a function f and a sequence s, write a function which returns a map. The keys should be the values of f applied to each item in s. The value at each key should be a vector of corresponding items in the order they appear in s.

...but without using group-by.

(ns live.test
  (:require [cljs.test :refer-macros [deftest is testing run-tests]]))
  
(defn group-seq [f s]
  )

(deftest test-63
  (is (= (group-seq #(> % 5) #{1 3 6 8}) {false [1 3], true [6 8]}))
  (is (= (group-seq #(apply / %) [[1 2] [2 4] [4 6] [3 6]]) {1/2 [[1 2] [2 4] [3 6]], 2/3 [[4 6]]}))
  (is (= (group-seq count [[1] [1 2] [3] [1 2 3] [2 3]]) {1 [[1] [3]], 2 [[1 2] [2 3]], 3 [[1 2 3]]})))
  
(run-tests)

To better understand the group-by fn that we are re-implementing, let's look at its source, as well as some examples from ClojureDocs:

user=> (source group-by)
(defn group-by 
  "Returns a map of the elements of coll keyed by the result of
  f on each element. The value at each key will be a vector of the
  corresponding elements, in the order they appeared in coll."
  {:added "1.2"
   :static true}
  [f coll]  
  (persistent!
   (reduce
    (fn [ret x]
      (let [k (f x)]
        (assoc! ret k (conj (get ret k []) x))))
    (transient {}) coll)))

We can group strings by their length:

(group-by count ["a" "as" "asd" "aa" "asdf" "qwer"])

Group integers by a predicate:

(group-by odd? (range 10))

Group by a primary key:

(group-by :user-id [{:user-id 1 :uri "/"} 
                    {:user-id 2 :uri "/foo"} 
                    {:user-id 1 :uri "/account"}])

Hey... how about if we use juxt to group by multiple criteria:

(def words ["Air" "Bud" "Cup" "Awake" "Break" "Chunk" "Ant" "Big" "Check"])
(group-by (juxt first count) words)

Let's look at each of our unit tests with group-by:

(group-by #(> % 5) #{1 3 6 8})
(group-by #(apply / %) [[1 2] [2 4] [4 6] [3 6]])
(group-by count [[1] [1 2] [3] [1 2 3] [2 3]])

The juxtification:

(defn group-seq [f s]
  (->> (map (juxt f identity) s)
       (reduce (fn [m [k v]]
                 (assoc m k (conj (get m k []) v)))
               {})))

(run-tests)

Breakdown:

(->> (map (juxt #(> % 5) identity) #{1 3 6 8})
       (reduce (fn [m [k v]]
                 (assoc m k (conj (get m k []) v)))
               {}))

Take a look at each part:

(map (juxt #(> % 5) identity) #{1 3 6 8})
(reduce (fn [m [k v]]
          (assoc m k (conj (get m k []) v)))
        {} '([false 1] [false 3] [true 6] [true 8]))
Tags: coding exercises KLIPSE 4clojure Cryogen juxt