4
votes

I'm trying to teach myself common lisp, and as an exercise in macro-writing, I'm trying to create a a macro to define a nested-do loop of arbitrary depth. I'm working with sbcl, using emacs and slime.

To start, I wrote this double-loop macro:

(defmacro nested-do-2 (ii jj start end &body body)
  `(do ((,ii ,start (1+ ,ii)))
       ((> ,ii ,end))
     (do ((,jj ,ii (1+ ,jj)))
         ((> ,jj ,end))
       ,@body)))

which I could then use as follows:

(nested-do-2 ii jj 10 20 (print (+ ii jj)))

BTW, I originally wrote this macro using gensym to generate the loop counters (ii, jj), but then I realized that the macro was pretty useless if I couldn't access the counters in the body.

Anyway, I would like to generalize the macro to create a nested-do loop that would be nested to an arbitrary level. This is what I've got so far, but it doesn't quite work:

(defmacro nested-do ((&rest indices) start end &body body)
  `(dolist ((index ,indices))
     (do ((index ,start (1+ index)))
          ((> index ,end))
        (if (eql index (elt ,indices (elt (reverse ,indices) 0)))
            ,@body))))

which I would like to invoke as follows:

(nested-do (ii jj kk) 10 15 (print (+ ii jj kk)))

However, the list is not being expanded properly, and I end up in the debugger with this error:

error while parsing arguments to DEFMACRO DOLIST:                                              
  invalid number of elements in                                                                
    ((INDEX (II JJ KK)))

And in case it's not obvious, the point of the embedded if statement is to execute the body only in the innermost loop. That doesn't seem terribly elegant to me, and it's not really tested (since I haven't been able to expand the parameter list yet), but it's not really the point of this question.

How can I expand the list properly within the macro? Is the problem in the macro syntax, or in the expression of the list in the function call? Any other comments will also be appreciated.

Thanks in advance.

2
Why not just treat the list of counters as a single argument? (defmacro nested-do indices start end &body body), and have (indices = '(ii jj kk))ApproachingDarknessFish
@ValekHalfHeart - that would also be fine, but I think the code is a little more self-documenting with (&rest indices) as opposed to just indices. And I was still unable to get indices to expand as I expected when I made that change.Ampers4nd
@huaiyuan - thanks for the link. Lots of interesting responses there. Your answer was concise and clean, and I also liked 6502's recursive macro expansion.Ampers4nd

2 Answers

1
votes

Here's one way to do it - build the structure from the bottom (loop body) up each index:

(defmacro nested-do ((&rest indices) start end &body body)
  (let ((rez `(progn ,@body)))
    (dolist (index (reverse indices) rez)
      (setf rez
            `(do ((,index ,start (1+ ,index)))
                 ((> ,index ,end))
               ,rez)))))
0
votes

[Aside from the down votes, this actually works and it is beautiful too!]

Just to clearly illustrate the recursive nature of the macro definition, here is a Scheme implementation:

(define-syntax nested-do
  (syntax-rules ()
    ((_ ((index start end)) body)
     (do ((index start (+ 1 index)))
         ((= index end))
       body))

    ((_ ((index start end) rest ...) body)
     (do ((index start (+ 1 index)))
         ((= index end))
       (nested-do (rest ...) body)))))

Using the above, as a template, something like this gets it done:

(defmacro nested-do ((&rest indices) start end &body body)
  (let ((index (car indices)))
    `(do ((,index ,start (1+ ,index)))
         ((> ,index ,end))
       ,(if (null (cdr indices))
            `(progn ,@body)
            `(nested-do (,@(cdr indices)) ,start ,end ,@body)))))


* (nested-do (i j) 0 2 (print (list i j)))
(0 0) 
(0 1) 
(0 2) 
(1 0) 
(1 1) 
(1 2) 
(2 0) 
(2 1) 
(2 2) 
NIL

Note that with all Common-Lisp macros you'll need to use the 'gensym' patterns to avoid variable capture.