Search code examples
clojuredictionary

Clojure map-longest


I am trying to write a Clojure utility function called map-longest (alternate name suggestion appreciated). This function will have the following "signature":

(map-longest fun missing-value-seq c1 & colls)

and will behave similarly to map, except than it will continue processing the supplied collections until the longest is exhausted. For collections shorter than the longest, when it runs out of values, it will take them from the missing-values-seq. It should be lazy, but obviously cannot be used with infinite collections.

Example use:

(print (apply str
  (map-longest #(str %1 \space %2 \space %3 \newline) (repeatedly "--")
    ["a1" "a2" "a3"] ["b1" "b2"] ["c1" "c2" "c3" "c4"])))

It should produce the following output:

a1 b1 c1
a2 b2 c2
a3 -- c3
-- -- c4

but I may have the call wrong.

How do I implement this? Does the clojure.core or clojure-contrib library already have something like this? As an alternative to missing-value-seq, would it be better to pass in a second function to generate the missing values (e.g.: #(identity "--") in my example)?

Use case: I am writing a small text spider solitaire player as an exercise in learning Clojure/functional programming. I need to be able to display the game tableaus (tableaux for purists :-)).


Solution

  • Here is a solution:

    (defn map-longest
      ([fn missing-value-fn c1]
        (map fn c1))
      ([fn missing-value-fn c1 & colls]
        (lazy-seq
          (when (not-every? empty? (conj colls c1))
            (let [firsts (map first (conj colls c1))]
              (cons
                (apply fn (map #(if (nil? %) (missing-value-fn) %) firsts))
                (apply map-longest
                  (conj (map rest colls) (rest c1) missing-value-fn fn))))))))
    

    Test:

    user=> (print (apply str 
             (map-longest #(str %1 \space %2 \space %3 \newline) #(identity "--") 
               ["a1" "a2" "a3"] ["b1" "b2"] ["c1" "c2" "c3" "c4"])))
    a1 b1 c1
    a2 b2 c2
    a3 -- c3
    -- -- c4
    nil
    

    Note that I have taken the missing-value-fn approach rather than the missing-value-seq one.

    Update

    Updated the code to take care of the case mentioned by ffriend in the comments.

    Test:

    user=> (print (apply str
              (map-longest #(str %1 \space %2 \space %3 \newline) #(identity "--")
                ["a1" "a2" nil] ["b1" "b2"] ["c1" "c2" nil "c4"])))
    a1 b1 c1
    a2 b2 c2
    -- -- --
    -- -- c4
    nil
    

    Please note that this will replace nils in the colls with the value returned by the missing-value-fn.