Clojure: map-occurrence


Sometimes you want behaviour that differs based on the number of times an item has been seen in a sequence. Clojure doesn't come with a function that does this. Before you say "What about frequencies?", frequencies gives you the total number of occurrences in a sequence of items, not the occurrence count.

Say we have the following sequence:

[:chest :dirt :chest :chest :dirt :chest :dirt]

We want to do different transformations based on which occurrence of the :chest keyword we are mapping over. For example: transforming the first occurrence into :diamond and the second into :ruby etc.

[:diamond :dirt :ruby :gold :dirt :grail :dirt]

Let's consider map-indexed for a minute.

(map-indexed (fn [i x] (if (= i 3) )) [7 8 9 10 4 9])

=>
(7 8 9 100 4 9)

It maps over a sequence passing the current index of item and the item into a function. In the example above we want to square the item at index 3 in the sequence.

We want to make a similar function except instead of the index we want to pass the occurrence of the item.

We can write a function with reduce that achieves this:

(defn map-occurrence [f s]
  (:result
   (reduce (fn [{:keys [result x-count] :as acc} x]
             (let [x-count (assoc x-count x (inc (get x-count x 0)))]
               (-> (update acc :result conj (f (x-count x) x))
                   (assoc :x-count x-count))))
           {:result [] :x-count {}}
           s)))

Personally, I find the reduce implementation quite dense and prefer a recursive solution.

(defn map-occurrence
  ([f s] (map-occurrence f s {}))
  ([f s x-count]
   (lazy-seq
    (when-let [[x & xs] (seq s)]
      (let [x-count (assoc x-count x (inc (get x-count x 0)))]
        (cons (f (x-count x) x)
              (map-occurrence f xs x-count)))))))

We use lazy-seq to define a function that recursively builds a list of items that are the result of (f (x-count x) x) where x-count is the current count of x. The use of (when-let [[x & xs] (seq s)] ...) is a common pattern when building lazy sequences, allowing you to apply a function to the head of the sequence and then call it recursively on the tail.

(map-occurrence
   (fn [occ item]
     (if (= item :chest)
       ({1 :diamond 2 :ruby 3 :gold 4 :grail} occ)
       item))
   [:chest :dirt :chest :chest :dirt :chest :dirt])

=>
(:diamond :dirt :ruby :gold :dirt :grail :dirt)

Honestly, the need for map-occurrence doesn't come up often but when it does it can be particularly useful tool.