What if map
and doseq
had a baby? I'm trying to write a function or macro like Common Lisp's mapc
, but in Clojure. This does essentially what map
does, but only for side-effects, so it doesn't need to generate a sequence of results, and wouldn't be lazy. I know that one can iterate over a single sequence using doseq
, but map can iterate over multiple sequences, applying a function to each element in turn of all of the sequences. I also know that one can wrap map
in dorun
. (Note: This question has been extensively edited after many comments and a very thorough answer. The original question focused on macros, but those macro issues turned out to be peripheral.)
This is fast (according to criterium):
(defn domap2
[f coll]
(dotimes [i (count coll)]
(f (nth coll i))))
but it only accepts one collection. This accepts arbitrary collections:
(defn domap3
[f & colls]
(dotimes [i (apply min (map count colls))]
(apply f (map #(nth % i) colls))))
but it's very slow by comparison. I could also write a version like the first, but with different parameter cases [f c1 c2]
, [f c1 c2 c3]
, etc., but in the end, I'll need a case that handles arbitrary numbers of collections, like the last example, which is simpler anyway. I've tried many other solutions as well.
Since the second example is very much like the first except for the use of apply
and the map
inside the loop, I suspect that getting rid of them would speed things up a lot. I have tried to do this by writing domap2 as a macro, but the way that the catch-all variable after &
is handled keeps tripping me up, as illustrated above.
Other examples (out of 15 or 20 different versions), benchmark code, and times on a Macbook Pro that's a few years old (full source here):
(defn domap1
[f coll]
(doseq [e coll]
(f e)))
(defn domap7
[f coll]
(dorun (map f coll)))
(defn domap18
[f & colls]
(dorun (apply map f colls)))
(defn domap15
[f coll]
(when (seq coll)
(f (first coll))
(recur f (rest coll))))
(defn domap17
[f & colls]
(let [argvecs (apply (partial map vector) colls)] ; seq of ntuples of interleaved vals
(doseq [args argvecs]
(apply f args))))
I'm working on an application that uses core.matrix matrices and vectors, but feel free to substitute your own side-effecting functions below.
(ns tst
(:use criterium.core
[clojure.core.matrix :as mx]))
(def howmany 1000)
(def a-coll (vec (range howmany)))
(def maskvec (zero-vector :vectorz howmany))
(defn unmaskit!
[idx]
(mx/mset! maskvec idx 1.0)) ; sets element idx of maskvec to 1.0
(defn runbench
[domapfn label]
(print (str "\n" label ":\n"))
(bench (def _ (domapfn unmaskit! a-coll))))
Mean execution times according to Criterium, in microseconds:
domap1: 12.317551 [doseq]
domap2: 19.065317 [dotimes]
domap3: 265.983779 [dotimes with apply, map]
domap7: 53.263230 [map with dorun]
domap18: 54.456801 [map with dorun, multiple collections]
domap15: 32.034993 [recur]
domap17: 95.259984 [doseq, multiple collections interleaved using map]
EDIT: It may be that dorun
+map
is the best way to implement domap
for multiple large lazy sequence arguments, but doseq
is still king when it comes to single lazy sequences. Performing the same operation as unmask!
above, but running the index through (mod idx 1000)
, and iterating over (range 100000000)
, doseq
is about twice as fast as dorun
+map
in my tests (i.e. (def domap25 (comp dorun map))
).
domap
with timings in the "appendix" section. As you can see, dorun+map (domap7
anddomap8
) is much slower thandoseq
(domap1
) anddotimes
(domap2
anddomap3
). (I resorted todotimes
because I couldn't figure out a more efficient way to walk collections in parallel (seedomap15
anddomap17
).) – Marsapply (partial map f)
. You reminded me that I can just sayapply map f
instead. – Marsdotimes
would be slow. Maybe I just haven't tried it on long enough collections. There should be a way to make a multiple-collection version that's as fast as the single-collectiondoseq
version. – Marsrun!
has been added to the core language. It doesn't quite do what I wanted, but it's related and it's worthwhile to know about it. – Mars