Clojure variadic macro iterating over sequences collected in & extra parameter
clojure, macros
Solution
Here is why your macro does not work:
`'(~@colls)` This expression creates a quoted list of all colls. E. g. if you pass it `(range 3)`, this expression becomes `'((range 3))`, so the literal argument will be one of your colls, preventing evaluation of `(range 3)` certainly not what you want here.
Now if you would not quote `(~@colls)` inside the macro, of course they would become a literal function invocation like `((range 3))`, which makes the compiler throw after macroexpansion time (it will try to eval `((0 1 2))`).
You can use `list` to avoid this problem:
(defmacro domap [f & colls]
`(dotimes [i# (apply min (map count (list ~@colls)))]
(apply ~f (map #(nth % i#) (list ~@colls)))))
=> (domap println (range 3))
0
1
2
However one thing here is terrible: Inside the macro, the entire list is created twice. Here is how we could avoid that:
(defmacro domap [f & colls]
`(let [colls# (list ~@colls)]
(dotimes [i# (apply min (map count colls#))]
(apply ~f (map #(nth % i#) colls#)))))
The colls are not the only thing that we need to prevent from being evaluated multiple times. If the user passes something like `(fn [& args] ...)` as `f`, that lambda would also be compiled in every step.
Now this is the exactly the scenario where you should ask yourself why you are writing a macro. Essentially, your macro has to make sure all arguments are eval'd without transforming them in any way before. Evaluation comes gratis with functions, so let's write it as a function instead:
(defn domap [f & colls]
(dotimes [i (apply min (map count colls))]
(apply f (map #(nth % i) colls))))
Given what you want to achieve, notice there is a function to solve that already, `dorun` which simply realizes a seq but does not retain the `head`. E. g.:
`(dorun (map println (range 3)))
would do the trick as well.
Now that you have `dorun` and `map`, you can simply compose them using `comp` to achieve your goal:
(def domap (comp dorun map))
=> (domap println (range 3) (range 10) (range 3))
0 0 0
1 1 1
2 2 2
Problem
Problem: How to handle a catch-all parameter after `&` in a macro, when the arguments to be passed are sequences, and the catch-all variable needs to be dealt with as a sequence of sequences? What gets listed in the catch-all variable are literal expressions. This is a macro that's intended to behave roughly Common Lisp's `mapc`, i.e. to do what Clojure's `map` does, but only for side-effects, and without laziness: ``` (defmacro domap [f & colls] `(dotimes [i# (apply min (map count '(~@colls)))] (apply ~f (map #(nth % i#) '(~@colls))))) ``` I've come to realize that this is not a good way to write `domap`--I got good advice about that in this question. However, I'm still wondering about the tricky macro problem that I encountered along the way. This works if the collection is passed as a literal: ``` user=> (domap println [0 1 2]) 0 1 2 nil ``` But doesn't work in other situations like this one: ``` user=> (domap println (range 3)) range 3 nil ``` Or this one: ``` user=> (def nums [0 1 2]) #'user/nums user=> (domap println nums) UnsupportedOperationException count not supported on this type: Symbol clojure.lang.RT.countFro (RT.java:556) ``` The problem is that it's literal expressions that are inside `colls`. This is why the macro `domap` works when passed a sequence of integers, but not in other situations. Notice the instances of `'(nums)`: ``` user=> (pprint (macroexpand-1 '(domap println nums))) (clojure.core/dotimes [i__199__auto__ (clojure.core/apply clojure.core/min (clojure.core/map clojure.core/count '(nums)))] (clojure.core/apply println (clojure.core/map (fn* [p1__198__200__auto__] (clojure.core/nth p1__198__200__auto__ i__199__auto__)) '(nums)))) ``` I've tried various combinations of `~`, `~@`, `'`, `let` with `var#`, etc. Nothing's worked. Maybe it's a mistake to try to write this as a macro, but I'd still be curious how to write a variadic macro that takes complex arguments like these.