Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure map-longest

Tags:

map

clojure

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

like image 626
Ralph Avatar asked Dec 16 '10 12:12

Ralph


2 Answers

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.

like image 171
Abhinav Sarkar Avatar answered Dec 06 '22 23:12

Abhinav Sarkar


This is not completely function you need, but a bit simplified version, so you could get the point:

(defn first-or-val [col missing]
  (if (empty? col)
    missing
    (first col)))

(defn map-longest [f missing-value & cols]
  (loop [cols cols, ret '()]
    (cond (every? empty? cols) (reverse ret)
          :else (recur (map rest cols)
                       (conj ret (apply f (map #(first-or-val % missing-value)
                                               cols)))))))

I omitted laziness, and you can add it easily with delay and force. I also changed missing-value-seq to just missing-value - I believe this is not the problem for you to replace it with sequence or generator.

Example:

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

Result:

a1 b1 c1
a2 b2 c2
a3 -- c3
-- -- c4
like image 30
ffriend Avatar answered Dec 06 '22 22:12

ffriend