Your macro is in my mapcar! (No! Your mapcar is in my macro!)

Macros are great. You can write code to write code. You can delay the evaluation of your arguments. You an remove all boiler plate code from your application. Yee-haw.

Mapcar is great. “Do this action to every item in this list and return a list of the actions’ return values.” For a lot of people this can be their first introduction to a function that takes a function as its argument. Woo-hoo.

Powerful stuff. You love it.

But what about the times when you want to mapcar with a macro?

CL-USER> (defun stupid (vars vals) 
           (mapcar #'defparameter vars vals)) 
STUPID 
CL-USER> (stupid '(x y z) '(43 54 65)) 
; Evaluation aborted: undefined function DEFPARAMETER

Hmmm…. This doesn’t work because defparameter’s a macro. Functions are first class citizens in the language, but macros aren’t.

Let’s start with a really silly example. We have a macro that takes a single argument… and we’ll use mapcar with a lambda function that constructs a list made up of the macro name and whatever value gets passed to it. It does that for each argument passed to it and uses eval to evaluate the resulting expression.

CL-USER> (defmacro z (var) 
           `(defparameter ,var 42)) 
Z 
CL-USER> (defun mapcro (macro &rest arguments) 
           (mapcar #'(lambda (x) 
                       (eval (list macro x))) 
                   arguments)) 
MAPCRO
CL-USER> (mapcro 'z 'a 'b 'c) 
(A B C) 
CL-USER> a 
42 
CL-USER> b 
42 
CL-USER> c 
42

Yeah, yeah. I know. “If you’re using eval then you’re doing something wrong.” There’s probably a cleaner way to do this, but I’m sure I’ve heard repeated complaints about macros not working near as good as functions. The fact that we have eval means that we can make things happen however we want regardless of how things are “supposed” to work. And if we ever get hung up on some wacky syntax issues, we can always brute force the issue in a pinch. Never mind the fact that we probably won’t get around to coming back later to clean it up!

Let’s keep tinkering with this, though. We want this to work with two variables instead of just one:

CL-USER> (defun mapcro (macro args1 args2) 
           (mapcar #'(lambda (x y) 
                       (eval (list macro x y))) 
                   args1 
                   args2)) 
MAPCRO
CL-USER> (mapcro 'defparameter '(k l m n) '(22 45 78 98)) 
(K L M N) 
CL-USER> k 
22 
CL-USER> l 
45 
CL-USER> m 
78 
CL-USER> n 
98

Okay, that’s getting somewhere…. We can use mapcar with something like defparameter now.

But what we really want is to have any number of argument lists. How do we handle that? The lambda has to know how many arguments it’s getting, right? Well… we can just use an &rest parameter and append them with the macro symbol insead of “listing” them. But if we’re passing in the argument lists via a &rest parameter to begin with, mapcar won’t know how to deal with them– it needs a set of list arguments, not a giant glob of lists!

This is complicated…. We’ll work it out at the repl prompt before rigging up the function:

CL-USER> (funcall #'mapcar
                  #'(lambda (&rest x) 
                       (eval (append (list 'defparameter) x))) 
                  '(x y z) 
                  '(55 66 77)) 
(X Y Z) 
CL-USER> x 
55 
CL-USER> y 
66 
CL-USER> z 
77

Oh wait… if I change the last arguments there to a list of lists, then it chokes! I should have used apply, not funcall:

CL-USER> (apply #'mapcar
                #'(lambda (&rest x) 
                    (eval (append (list 'defparameter) x))) 
                '((u v w) (34 53 63))) 
(U V W) 
CL-USER> u 
34 
CL-USER> v 
53 
CL-USER> w 
63

Okay… now we can fix our function to make it work for any number of arguments….

CL-USER> (defun mapcro (macro &rest args) 
           (apply #'mapcar
                  #'(lambda (&rest x) 
                      (eval (append (list macro) x))) 
                  args)) 
MAPCRO
CL-USER> (mapcro 'defparameter '(jj kk ll) '(77 87 97)) 
(JJ KK LL) 
CL-USER> jj
77 
CL-USER> kk
87 
CL-USER> ll 
97

We did it! We feel really guilty, but we did it! Party!

Oh wait… typing all of those quotes is a hassle. Just for fun, let’s rig up a macro to rearrange everything for us so we don’t have to bother with them:

CL-USER> (defmacro mapcro (macro &rest args) 
           `(apply #'mapcar
             #'(lambda (&rest x) 
                 (eval (append (list ',macro) x))) 
             ',args)) 
MAPCRO
CL-USER> (mapcro defparameter (mm nn oo pp) (63 74 85 96)) 
(MM NN OO PP) 
CL-USER> mm 
63 
CL-USER> nn
74 
CL-USER> oo
85 
CL-USER> pp 
96

Notice that macros and functions have their own namespaces. Anywhere in the code that we have a “mapcro” symbol on the far left side of an expression will trigger the macro expansion before anything else happens. We don’t have a naming conflict because we’re using apply with an explicit function reference.

(Yeah, this is probably completely evil or pointless somehow, but I wanted to do something like this a couple months ago and struggled to get something to work. It just hit me the other that that eval means I can do this even whether I’m supposed to or not….)

2 Responses to “Your macro is in my mapcar! (No! Your mapcar is in my macro!)”

  1. Larry Clapp Says:

    I tried your last mapcro and got “The variable ARGS is unbound.” You have backquotes in front of ,macro and ,args, whereas you should have normal single-quotes.

    And you’re right, there is a cleaner way to do this. Your code expands into code that loops over the arguments and uses EVAL to run the code you cons up:

    CL-USER 35 > (pprint (macroexpand-1 ‘(mapcro defparameter (mm nn oo pp) (63 74 85 96))))

    (APPLY #’MAPCAR
    #'(LAMBDA (&REST X) (EVAL (APPEND (LIST ‘DEFPARAMETER) X)))
    ‘((MM NN OO PP) (63 74 85 96)))

    whereas you should loop over the arguments in the macro itself and return a list for the REPL to evaluate. Consider:

    (defmacro mapcro (macro &rest args)
    `(progn ,@(apply #’mapcar
    (lambda (&rest args2)
    `(,macro ,@args2))
    args)))

    and its expansion

    CL-USER 36 > (pprint (macroexpand-1 ‘(mapcro defparameter (mm nn oo pp) (63 74 85 96))))

    (PROGN
    (DEFPARAMETER MM 63)
    (DEFPARAMETER NN 74)
    (DEFPARAMETER OO 85)
    (DEFPARAMETER PP 96))

  2. Anonymous Says:

    How about

    (defun mapcro (macro &rest args)
    `(progn
    ,@(apply #’mapcar (lambda (&rest x) (list* macro x)) args)))

Leave a comment