Continuing from part 4, it’s time to start beefing up the macro a bit. Now, we might want our loop to start somewhere other than the beginning of the list, and end somewhere other than the end of the list. And, a few articles back, I mentioned that these lists might sometimes be circular, so we’d like to be able to start at the fifth element of the list, loop forward to the end, continue at the beginning, and then loop up to, say, the second element of the list.
At this point, we come across another very helpful feature of Lisp, one that I frequently miss when programming in C++. Named parameters. A function that takes four or five parameters can be awkward to read where it appears in source code, because it might not be clear which parameters represent what behaviour. It interferes with readability. In Lisp, you can require certain parameters to be named, which helps to understand what is happening. A nice consequence of this is that you can set defaults for any of the named parameters, in any order, whereas in C++ you are fairly tightly restricted in what default parameter values you may omit (you cannot choose to omit the second, fourth, and seventh parameters of the invocation, you must omit a contiguous set of trailing parameters with defaults).
So, now we’re going to set up our macro. Once again, I use (gensym) to build capture variables to avoid multiple evaluations of the arguments. I also define named parameters :start and :end, which take iterator values as arguments, and :circular, which is either nil or non-nil. I’ve also created here two lambda functions. These allow me to create a tiny function on the spot, where the programmer can see it because it’s in the screen with the implementation of its use. One lambda function is the alternative iterator increment function, to be used when the :circular argument is non-nil, to force the iterator to loop back once it reaches the end of the list. The other is an exit test that checks to see if the iterator matches the :end condition, if one was supplied. If so, the (do…) loop is exited at that point. Note that this condition is tested after the user-supplied body of the loop, before the iterator increment operation, so that the :end iterator is included in the loop.
Here is the next version of this macro:
(defmacro iter-loop ((dl-list iter &key start end circular) &body body) (let ((dl-cap (gensym)) (start-cap (gensym)) (end-cap (gensym)) (circ-cap (gensym)) (inc-fcn (gensym)) (early-exit (gensym))) `(let((,dl-cap ,dl-list) (,start-cap ,start) (,end-cap ,end) (,circ-cap ,circular) (,inc-fcn 'iter-next) ,early-exit) (when ,circ-cap (setf ,inc-fcn #'(lambda (dl it) (let ((rv (iter-next dl it))) (or rv (iter-front dl)))))) (unless ,start-cap (setf ,start-cap (iter-front ,dl-cap))) (when ,end-cap (setf ,early-exit #'(lambda (x) (eq x ,end-cap)))) (do ((,iter ,start-cap (funcall ,inc-fcn ,dl-list ,iter))) ((not ,iter)) ,@body (when (and ,early-exit (funcall ,early-exit ,iter)) (return))))))