Reputation: 14918
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
usingreduce
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
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
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 seq
s, 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
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
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
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
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