Continuing from part 6, we’re now getting into some macro magic. We noticed a pattern between the basic iter-loop, 2-iter-loop, and 3-iter-loop. We create a number of iterator names as placeholders. The first one, we initialize from the beginning of the dl-list. The second one we assign and update to the iter-next of the first iterator. The third iterator we assign and update to the iter-next of the second iterator, and so on. The exit condition is when the last iterator is nil. Can we automate this? Can we make a macro that produces macros of any number of iterators? Yes. Here’s the basic version of that macro:
(defmacro create-n-iter-loop (n name) (assert (and (integerp n) (> n 0))) (let (symbols first-sym last-sym) (dotimes (i n) (push (intern (format nil "ITER~D" i)) symbols)) (setf last-sym (first symbols)) (setf symbols (reverse symbols)) (setf first-sym (first symbols)) `(defmacro ,name ((dl-list ,@symbols) &body body) (let ((dl-cap (gensym))) `(let ((,dl-cap ,dl-list)) (do* ((,,first-sym (iter-front ,dl-cap) (iter-next ,dl-cap ,,first-sym)) ,,@(mapcar #'(lambda (x y) ``(,,x (and ,,y (iter-next ,,'dl-cap ,,y)) (and ,,y (iter-next ,,'dl-cap ,,y)))) (cdr symbols) symbols)) ((not ,,last-sym)) ,@body))))))
This macro would be invoked with an ‘n’, and the name of the looping macro to be created. So, executing the command
(create-n-iter-loop 1 iter-loop)
produces the simple single-iterator macro. We can observe this by executing the command
(macroexpand-1 '(create-n-iter-loop 1 iter-loop))
the output of which is:
(DEFMACRO ITER-LOOP ((DL-LIST::DL-LIST ITER0) &BODY DL-LIST::BODY) (LET ((DL-LIST::DL-CAP (GENSYM))) `(LET ((,DL-LIST::DL-CAP ,DL-LIST::DL-LIST)) (DO* ((,ITER0 (DL-LIST:ITER-FRONT ,DL-LIST::DL-CAP) (DL-LIST:ITER-NEXT ,DL-LIST::DL-CAP ,ITER0))) ((NOT ,ITER0)) ,@DL-LIST::BODY))))
This code is the same as our original iter-loop. Similarly, we can produce the 2-iter-loop:
(DEFMACRO 2-ITER-LOOP ((DL-LIST::DL-LIST ITER0 ITER1) &BODY DL-LIST::BODY) (LET ((DL-LIST::DL-CAP (GENSYM))) `(LET ((,DL-LIST::DL-CAP ,DL-LIST::DL-LIST)) (DO* ((,ITER0 (DL-LIST:ITER-FRONT ,DL-LIST::DL-CAP) (DL-LIST:ITER-NEXT ,DL-LIST::DL-CAP ,ITER0)) (,ITER1 (AND ,ITER0 (DL-LIST:ITER-NEXT ,DL-LIST::DL-CAP ,ITER0)) (AND ,ITER0 (DL-LIST:ITER-NEXT ,DL-LIST::DL-CAP ,ITER0)))) ((NOT ,ITER1)) ,@DL-LIST::BODY))))
and the 3-iter-loop macro:
(DEFMACRO 3-ITER-LOOP ((DL-LIST::DL-LIST ITER0 ITER1 ITER2) &BODY DL-LIST::BODY) (LET ((DL-LIST::DL-CAP (GENSYM))) `(LET ((,DL-LIST::DL-CAP ,DL-LIST::DL-LIST)) (DO* ((,ITER0 (DL-LIST:ITER-FRONT ,DL-LIST::DL-CAP) (DL-LIST:ITER-NEXT ,DL-LIST::DL-CAP ,ITER0)) (,ITER1 (AND ,ITER0 (DL-LIST:ITER-NEXT ,DL-LIST::DL-CAP ,ITER0)) (AND ,ITER0 (DL-LIST:ITER-NEXT ,DL-LIST::DL-CAP ,ITER0))) (,ITER2 (AND ,ITER1 (DL-LIST:ITER-NEXT ,DL-LIST::DL-CAP ,ITER1)) (AND ,ITER1 (DL-LIST:ITER-NEXT ,DL-LIST::DL-CAP ,ITER1)))) ((NOT ,ITER2)) ,@DL-LIST::BODY))))
So, why go to all that trouble of generalizing the macro? Well, I need all three looping constructs. iter-loop, 2-iter-loop, 3-iter-loop. They’re very similar macros, but coding them three times raises the possibility that there will be typos that introduce subtle errors, or bugs might be fixed in two of the macros, but not in the third. By making sure that they’re all created from a single block of code, I increase the reliability. I can make sure that all of the macros have the same API, that they use the same keywords, and if one day I decide I need a 4-iter-loop, it’s no effort at all to create it and know that it’s running the same basic code as all the others.
So, now that we’ve written the generalized basic iterator macro-generating macro, it’s time to put back the bells and whistles. Circular lists, start/end points, and backward traversal. That’s for next time.