4 October 2021

Mapping over multiple collections in Clojure

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

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. https://projecteuler.net/problem=22)

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