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 :-)).
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 nil
s in the colls with the value returned by the missing-value-fn
.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With