mluisbrown
mluisbrown

Reputation: 14918

How to implement map using reduce in Clojure

In the book Clojure for the Brave and True at the end of the section covering reduce there's a challenge:

If you want an exercise that will really blow your hair back, try implementing map using reduce

It turns out that this was a lot harder (at least for me, a Clojure beginner) than I thought it would be. After quite a few hours I came up with this:

(defn map-as-reduce
  [f coll]
  (reduce #(cons (f %2) %1) '() (reverse coll)))

Is a better way to do this? I'm particularly frustrated by the fact that I have to reverse the input collection in order for this to work correctly. It seems somehow inelegant!

Upvotes: 15

Views: 3510

Answers (6)

Enrique Montalvo
Enrique Montalvo

Reputation: 91

The real map calls seq on its collection argument(s) and returns a lazy seq, so maybe this to get it a little closer to the real map?

(defn my-map
  [f coll]
  (lazy-seq (reduce #(conj %1 (f %2)) [] (seq coll))))

I would have added that as a comment, but I don't have the reputation. :)

Upvotes: 7

Svante
Svante

Reputation: 51501

It is more common to reverse the result, not the input. This is a common idiom when handling singly-linked lists in a recursive fashion. It preserves linear complexity with this data structure.

You might want to offer different implementations for other seqs, e. g., vectors, maybe based on conj instead of cons.

I would not worry too much about elegance with this kind of exercise.

Upvotes: 2

Lewix
Lewix

Reputation: 999

As it was already pointed out. You do not have to reverse the input. cons add an item to the beginning of a sequence (even on vectors) whereas conj will always add in the most natural way, it always add an item in the fastest way possible for the collection. it will add from left to right for list, and from left to right for vectors.

I noticed that most suggested answers use 'reduce' so allow me to suggest this one using mainly recursion:

(defn my-map [f coll]
 (loop [f f coll coll acc []]
   (if (empty? coll)
     acc
     (recur f (rest coll) (conj acc (f (first coll)))))))

Upvotes: 1

leetwinski
leetwinski

Reputation: 17859

just for fun: map really accepts more than one collection as an argument. Here is an extended implementation:

(defn map-with-reduce
  ([f coll] (seq (reduce #(conj %1 (f %2)) [] coll)))
  ([f coll & colls]
    (let [colls (cons coll colls)]
      (map-with-reduce (partial apply f)
                       (partition (count colls) 
                                  (apply interleave colls))))))

in repl:

user> (map-with-reduce inc [1 2 3])
(2 3 4)

user> (map-with-reduce + [1 2 3] [4 5] [6 7 8])
(11 14)

Upvotes: 13

Adam Lee
Adam Lee

Reputation: 1894

You can use conj to append to a vector instead of prepending to a list:

(defn my-map [f coll]
  (reduce (fn [result item]
              (conj result (f item)))
          [] coll))

(my-map inc [1 2 3]) => [2 3 4] 

Upvotes: 2

Sam Estep
Sam Estep

Reputation: 13324

Remember that you can efficiently insert at the end of a vector:

(defn map' [f coll]
  (reduce #(conj %1 (f %2)) [] coll))

Example:

(map' inc [1 2 3])
;=> [2 3 4]

One difference between this map' and the original map is that the original map returns an ISeq instead of just a Seqable:

(seq? (map inc [1 2 3]))
;=> true

(seq? (map' inc [1 2 3]))
;=> false

You could remedy this by composing the above implementation of map' with seq:

(defn map' [f coll]
  (seq (reduce #(conj %1 (f %2)) [] coll)))

The most important difference now is that, while the original map is lazy, this map' is eager, because reduce is eager.

Upvotes: 13

Related Questions