Reputation: 8854
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.
Upvotes: 3
Views: 538
Reputation: 9276
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
Upvotes: 5