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.