简体   繁体   中英

How do I write this macro without using `eval`?

I am trying to write a macro that creates an arbitrary number of nested loops and executes some code at each iteration of the loops. In my first attempt (shown below), the macro returned the code instead of running it.

;; WRONG! Returns a bunch of nested loops instead of evaluating the code.

(defmacro do-combinations ((var lists) &body body)
  `(let* ((lst (mapcar #'(lambda (x)
                           `(loop for ,(gensym) in (list ,@x) do))
                       ,lists))
          (symbols (mapcar #'caddr lst)))
     (reduce #'(lambda (x y) `(,@y ,x))
             lst
             :initial-value `(let ((,',var (list ,@symbols)))
                               (progn ,',@body)))))
CL-USER 25 : 1 > (do-combinations (n '((1 2 3)
                                       (10 20 30)
                                       (100 200 300)))
                   (pprint n))
(LOOP FOR #:G872 IN (LIST 100 200 300)
      DO (LOOP FOR #:G871 IN (LIST 10 20 30)
               DO (LOOP FOR #:G870 IN (LIST 1 2 3)
                        DO (LET # #))))

My cack-handed fix of last resort for this was inserting an eval

;; Ugly fix with eval
(defmacro do-combinations ((var lists) &body body)
  `(let* ((lst (mapcar #'(lambda (x)
                           `(loop for ,(gensym) in (list ,@x) do))
                       ,lists))
          (symbols (mapcar #'caddr lst)))
     (eval (reduce #'(lambda (x y) `(,@y ,x))
                   lst
                   :initial-value `(let ((,',var (list ,@symbols)))
                                     (progn ,',@body))))))    

CL-USER 35 : 1 > (do-combinations (n '((1 2 3)
                                       (10 20 30)
                                       (100 200 300)))
                   (pprint n))

(1 10 100)
(2 10 100)
...

The fix does work (sort of), but looks dreadful. How would you write this macro more elegantly without resorting to eval ?

There are a bunch of basic problems (like what code should be generated and when) in an already mildly complex macro. You might think about doing simpler macro examples first. But one might be able to get your code working, so not all is lost.

Let's look at some of the problems:

How to use your macro in code

You want to use your macro like this:

(do-combinations (n '((1 2 3)
                      (10 20 30)
                      (100 200 300)))
  (pprint n))

But it makes no sense to quote the nested list. A macro generates code probably at compile time and at that time the list needs to be known. Thus there is no way you can or should evaluate this. Thus one can remove the quote:

(do-combinations (n ((1 2 3)
                     (10 20 30)
                     (100 200 300)))
  (pprint n))

Some macro basics

Now when you write a macro there are these basic things to understand:

  • a macro will generate code. You need to know what code your macro should generate. Write the code down and compare it to what your macro does.
  • to see what the macro generates use macroexpand and macroexpand-1 . Use pprint to pretty print the resulting code.

Let's look at the code being generated

Now let's look at the code your macro generates:

CL-USER 145 > (pprint
               (macroexpand-1 '(do-combinations (n ((1 2 3)
                                                    (10 20 30)
                                                    (100 200 300)))
                                 (pprint n))))

(LET* ((LST
        (MAPCAR #'(LAMBDA (X) `(LOOP FOR ,(GENSYM) IN (LIST ,@X) DO))
                ((1 2 3) (10 20 30) (100 200 300))))
       (SYMBOLS (MAPCAR #'CADDR LST)))
  (REDUCE #'(LAMBDA (X Y) `(,@Y ,X))
          LST
          :INITIAL-VALUE
          `(LET ((N (LIST ,@SYMBOLS))) (PROGN (PPRINT N)))))

You can see that it is all wrong, since there is lots of code generated which should run at macro expansion time - not at runtime! It's not at all generating nested loops.

You can see in your macro this second line:

`(let* ((lst (mapcar #'(lambda (x)

It means that the code will be generated. But you probably want to run it in the expansion phase.

A better version

Here is a version which has the right code generation:

(defmacro do-combinations ((var lists) &body body)
  (let* ((lst (mapcar #'(lambda (x)
                           `(loop for ,(gensym) in (list ,@x) do))
                       lists))
         (symbols (mapcar #'caddr lst)))
     (reduce #'(lambda (x y) `(,@y ,x))
             lst
             :initial-value `(let ((,var (list ,@symbols)))
                               ,@body))))

Let's see:

CL-USER 147 > (pprint
               (macroexpand-1 '(do-combinations (n ((1 2 3)
                                                    (10 20 30)
                                                    (100 200 300)))
                                 (pprint n))))

(LOOP FOR #:G424120 IN (LIST 100 200 300)
      DO (LOOP FOR #:G424119 IN (LIST 10 20 30)
               DO (LOOP FOR #:G424118 IN (LIST 1 2 3)
                        DO (LET ((N (LIST #:G424118 #:G424119 #:G424120)))
                             (PPRINT N)))))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM