Mars
Mars

Reputation: 8854

Clojure variadic macro iterating over sequences collected in & extra parameter

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

Answers (1)

Leon Grapenthin
Leon Grapenthin

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

Related Questions