In which a silly old bear does useless exercises with lisp for personal amusement.
In my last posting I created a simple macro to ‘tweak’ the predicate of a while loop. If the predicate was < something then I changed it to <= something. Anyone who looked at the macro may be asking a few questions.
(defmacro while2 (test &body body)
(let ((lastpart (cdr test)))
(if (eq (car test) '<)
`(do ()
((not (or ,test
(eql ,@lastpart))))
,@body)
`(do ()
((not ,test))
,@body))))
Why didn’t you just use (<= ,@lastpart), or even replace the (not (< ,@lastpart)) with (> ,@lastpart)?
And they’d be right, of course, but I was trying to use the section to learn a thing or two about lisp. I wanted to simulate the case where we were evaluating something more than once where the user expected it to be evaluated only once.
Using either of the above replacements would cause the macro to work as expected. As it is written it doesn’t work as expected. This is actually by design, so be nice and play along with me!
Here is what happens when I call Grahams macro with some new code.
CL-USER> (while (< (incf x) 5)
(format t "I'm at ~a~&" x))
I'm at 1
I'm at 2
I'm at 3
I'm at 4
NIL
CL-USER> x
5
No big surprises there, but then I ran my macro which we would expect to print out 1 through 5 since the macro is intending to do the same thing as if I had replaced the < in the while2 expression with <=
CL-USER> (setq x 0)
0
CL-USER> (while2 (< (incf x) 5)
(format t "I'm at ~a~&" x))
I'm at 1
I'm at 2
I'm at 3
I'm at 4
NIL
Whoops, what went wrong? Why did the macro not run as expected? The value after the run is what we expected, but the output is not.
CL-USER> x
6
We start to see what happened when we look at the expansion of the macro . ..
(DO () ((NOT (OR (< (INCF X) 5) (EQL (INCF X) 5)))) (FORMAT T "I'm at ~a~&" X))
when (incf x) is equal to 5, the first condition fails so we check to see if (incf x) is equal to 5. By evaluating the expression again, the current value is now 6 and it’s not equal to 5 so we break from our loop.
The problem here is that we are evaluating (incf x) two times in some cases when we really want to evaluate only once.
Again, we are working with an artificial problem because we could solve the whole thing rather nicely by just replacing < with <= in our macro. We are solving the case for where we really would need to evaluate something in a similar fashion as we currently our with our where2 macro.
What we need to do is assign all the potential arguments to a gensym and then use the gensym in our expression like this:
(defmacro while2 (test &body body)
(let* ((lastpart (cdr test))
(symlist (mapcar (lambda (x) (list (gensym) x)) lastpart)))
(if (eq (car test) '<)
`(do ()
((let ,symlist
(not (or (< ,@(mapcar #'car symlist))
(eql ,@(mapcar #'car symlist))))))
,@body)
`(do ()
((not ,test))
,@body))))
now if we expand the macro we get:
(macroexpand-1 '(while2 (< (incf x) 5)
(format t "I'm at ~a~&" x)))
(DO ()
((LET ((#:G1906 (INCF X)) (#:G1907 5))
(NOT (OR (< #:G1906 #:G1907) (EQL #:G1906 #:G1907)))))
(FORMAT T "I'm at ~a~&" X))
And it works fine:
(setq x 0)
(while2 (< (incf x) 5)
(format t "I'm at ~a~&" x))
I'm at 1
I'm at 2
I'm at 3
I'm at 4
I'm at 5
CL-USER> x
6
Except when we throw it a curve ball:
(setq x 0)
(while2 (< (incf x) 5 6)
(format t "I'm at ~a~&" x))
invalid number of arguments: 3
So we change it so that we check whether each element is less than or equal. (Yes, yes, if we wouldn’t have had a <= operator, we would have been much smarter to write just that piece in the beginning, but I had to see this through)
(defmacro while2 (test &body body)
(let* ((lastpart (cdr test))
(symlist (mapcar (lambda (x) (list (gensym) x)) lastpart)))
(if (eq (car test) ‘<)
`(do ()
((let ,symlist
(not (every (lambda (z) (or (< ,(caar symlist) z)
(eql ,(caar symlist) z)))
(list ,@(mapcar #‘car (cdr symlist)))))))
,@body)
`(do ()
((not ,test))
,@body))))
Which expands to:
(DO ()
((LET ((#:G2002 (INCF X)) (#:G2003 5) (#:G2004 6))
(NOT (EVERY (LAMBDA (Z) (OR (< #:G2002 Z) (EQL #:G2002 Z))) (LIST #:G2003 #:G2004)))))
(FORMAT T "I'm at ~a~&" X))
Which finally does the exact same thing it would do as if we called it with the <= that we wanted in the first place
(defmacro while2 (test &body body)
(let ((lastpart (cdr test)))
(if (eq (car test) '<)
`(do ()
((not (<= ,@lastpart)))
,@body)
`(do ()
((not ,test))
,@body))))