Mapping over multiple collections in Clojure

The map function maps 'n' collections to functions that recieve 'n' arguments.

October 4, 2021

The introductory statement above just about says it all. Here's a concrete example of that principle applied to solving problem 22 of "Project Euler". (Make sure you read the question description to understand the following code.

We start with the input:

(def input ["MARY", "PATRICIA", "LINDA", ...

We test-drive a function for calculating the 'alphabetic' score:

(defn alpha-score [word]
  (->> (string/lower-case word)
       (map #(inc (- (int %) (int \a))))
       (apply +)))

This function does the following:

(it "computes alphabetic score"
  (should= 6 (alpha-score "abc"))
  (should= 53 (alpha-score "COLIN")))

We test-drive a function for calculating the 'name' score:

(defn name-score [rank word]
  (* rank (alpha-score word)))

Supposing that the name "COLIN" is the 938th name in the list:

(it "computes name score"
  (should= 49714 (name-score 938 "COLIN")))

Now, to solve the problem we must:

  1. Sort the input names in alphabetical order
  2. Interleave the indices of each name in the list with each name
  3. Make pairs of each index/name combo.
  4. Pass each index/name pair to the name-score function
  5. Sum the results of each and ever call to name-score

Here's a literal implementation of the above steps:

(defn solve-1 []
  (let [indices (range 1 (inc (count input)))]
    (as-> #_"step 1" (sort input) $
          #_"step 2" (interleave indices $)
          #_"step 3" (partition 2 $)
          #_"step 4" (map #(name-score (first %1) (second %1)) $)
          #_"step 5" (apply + $))))

It seems that steps 2 and 3 are really just a mapping of two collections over a function that receives two arguments...WHICH IS WHAT THE MAP FUNCTION ALREADY DOES!

Most of the time when we use map we are mapping a single collection over a function that receives that argument. But map is much more flexible in that it expects 'n' collections and a function that receives 'n' arguments.

Here's a more succinct implementation which makes use of this knowledge:

(defn solve-2 []
  (let [indices (range 1 (inc (count input)))]
    (as-> #_"step 1   " (sort input) $
          #_"steps 2-4" (map name-score indices $)
          #_"step 5   " (apply + $))))

(Of course, we could use ->> ('thread-last') instead of as-> ('thread-as'), but I like how 'thread-as' overtly displays both collections being passed to the map function in this case.)