6
votes

I need to modify map function behavior to provide mapping not with minimum collection size but with maximum and use zero for missing elements.

Standard behavior:

(map + [1 2 3] [4 5 6 7 8]) => [5 7 9]

Needed behavior:

(map + [1 2 3] [4 5 6 7 8]) => [5 7 9 7 8]

I wrote function to do this, but it seems not very extensible with varargs.

(defn map-ext [f coll1 coll2]
  (let [mx (max (count coll1) (count coll2))]
    (map f
     (concat coll1 (repeat (- mx (count coll1)) 0))
     (concat coll2 (repeat (- mx (count coll2)) 0)))))

Is there a better way to do this?

6

6 Answers

7
votes

Your method is concise, but inefficient (it calls count). A more efficient solution, which does not require the entirety of its input sequences to be stored in memory follows:

(defn map-pad [f pad & colls]
  (lazy-seq
   (let [seqs (map seq colls)]
     (when (some identity seqs)
       (cons (apply f (map #(or (first %) pad) seqs))
             (apply map-pad f pad (map rest seqs)))))))

Used like this:

user=> (map-pad + 0 [] [1] [1 1] (range 1 10))
(3 3 3 4 5 6 7 8 9)

Edit: Generalized map-pad to arbitrary arity.

6
votes

Another lazy variant, usable with an arbitrary number of input sequences:

(defn map-ext [f ext & seqs]
  (lazy-seq
   (if (some seq seqs)
     (cons (apply f (map #(if (seq %) (first %) ext) seqs))
           (apply map-ext f ext (map rest seqs)))
     ())))

Usage:

user> (map-ext + 0 [1 2 3] [4 5 6 7 8])
(5 7 9 7 8)

user> (map-ext + 0 [1 2 3] [4 5 6 7 8] [3 4])
(8 11 9 7 8)
4
votes

If you just want it to work for any number of collections, try:

(defn map-ext [f & colls] 
  (let [mx (apply max (map count colls))]
      (apply map f (map #(concat % (repeat (- mx (count %)) 0)) colls))))

Clojure> (map-ext + [1 2] [1 2 3] [1 2 3 4])
(3 6 6 4)

I suspect there may be better solutions though (as Trevor Caira suggests, this solution isn't lazy due to the calls to count).

1
votes

How about that:

(defn map-ext [f x & xs]
  (let [colls (cons x xs)
        res   (apply map f colls)
        next  (filter not-empty (map #(drop (count res) %) colls))]
    (if (empty? next) res
        (lazy-seq (concat res (apply map-ext f next))))))

user> (map-ext + [1 2 3] [4] [5 6] [7 8 9 10]) 
(17 16 12 10)
0
votes

Along the lines of @LeNsTR's solution, but simpler and faster:

(defn map-ext [f & colls]
  (lazy-seq
   (let [colls (filter seq colls)
         firsts (map first colls)
         rests (map rest colls)]
    (when (seq colls)
      (cons (apply f firsts) (apply map-ext f rests))))))

(map-ext + [1 2 3] [4] [5 6] [7 8 9 10])
;(17 16 12 10)

I've just noticed Michał Marczyk's accepted solution, which is superior: it deals properly with asymmetric mapping functions such as -.

0
votes

We can make Michał Marczyk's answer neater by using the convention - which many core functions follow - that you get a default or identity value by calling the function with no arguments. For examples:

(+) ;=> 0
(concat) ;=> ()

The code becomes

(defn map-ext [f & seqs]
  (lazy-seq
   (when (some seq seqs)
     (cons (apply f (map #(if (seq %) (first %) (f)) seqs))
           (apply map-ext f (map rest seqs)))
     )))

(map-ext + [1 2 3] [4 5 6 7 8] [3 4])
;(8 11 9 7 8)

I've made the minimum changes. It could be speeded up a bit.

We may need a function that will inject such a default value into a function that lacks it:

(defn with-default [f default]
  (fn
    ([] default)
    ([& args] (apply f args))))

((with-default + 6)) ;=> 6
((with-default + 6) 7 8) ;=> 15

This could be speeded up or even turned into a macro.