4

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.

Vsevolod Dyomkin
  • 9,343
  • 2
  • 31
  • 36
Ampers4nd
  • 787
  • 1
  • 11
  • 20
  • 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 Mar 14 '13 at 06:00
  • 1
    See [this question](http://stackoverflow.com/q/10163298/16240). – huaiyuan Mar 14 '13 at 08:38
  • @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 Mar 14 '13 at 15:43
  • @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 Mar 14 '13 at 15:46

2 Answers2

1

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)))))
Vsevolod Dyomkin
  • 9,343
  • 2
  • 31
  • 36
  • The inner indices should run from the respective current value of the previous index. – Svante Mar 14 '13 at 13:55
  • This works beautifully. Thanks. However, I am still curious as to why the code that I posted above doesn't expand. I think I must be missing something in the quoting, which I'm still inexperienced with. Any thoughts? – Ampers4nd Mar 14 '13 at 15:40
  • 2
    There're 2 problems in this line: `(dolist ((index ,indices))` - `dolist` needs not double parents (`((`) and `,indices` expand as `(ii jj kk)`, but they should expand as `(list ii jj kk)`, otherwise they are treated as function application of `ii` with arguments `jj` and `kk`. So the line should be: `(dolist (index (list ,@indices))` – Vsevolod Dyomkin Mar 14 '13 at 17:28
0

[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.

GoZoner
  • 67,920
  • 20
  • 95
  • 145
  • +1 for the recursive macro. Note that no gensyms are needed for this macro, since no new symbols are being introduced into the environment. – Clayton Stanley Mar 15 '13 at 06:18
  • The local 'index' is being introduced. – GoZoner Mar 15 '13 at 15:48
  • `index` is not being introduced. The expansion of your example is `(DO ((I 0 (1+ I))) ((> I 2)) (DO ((J 0 (1+ J))) ((> J 2)) (PROGN (PRINT (LIST I J)))))`. There's no `index` in there. – lmj Mar 15 '13 at 20:02
  • Also a minor point regarding style: I would recommend writing `,(foo)` instead of `(,@(foo))`. – lmj Mar 15 '13 at 20:05
  • 1
    You and @clayton are right, no symbol is introduced. Whereas `,(foo)` might work, I used `(,@(car indices))` so that the outside parens can be easily seen as syntactic elements of `(nested-do (...) ...)` – GoZoner Mar 15 '13 at 20:17
  • FWIW, I agree with GoZoner's stylistic use of (,@(foo)) in this case. But we're just splitting hairs :) – Clayton Stanley Mar 15 '13 at 20:37