Generalized Variables and Macros 2

Yesterday we talked about some of the difficulties in using setf in a macro and how these difficulties can sometimes be overcome by using the mysterious define-modify-macro. Today, I want to look at a more general approach that will also explain how define-modify-macro works.

Last time, we wrote the += macro to implement the C += operator. That worked out well so we might decide to do a -= and then a *= and so on. After awhile we realize that we’re repeating the same pattern over and over so we decide to write a general version, op=, that will take the operator we want as an argument. That is we want to write

(op= + x y)

instead of

(+= x y)

That way we have only one macro and it works for any operator:

(op= floor x y) → (setf x (floor x y))

We can’t use define-modify-macro here because it requires the name of the function at the time of the call to define-modify-macro and we want to make op= take the function as an argument. We also can’t write

(defmacro op= (op x y)
  `(setf ,x (,op ,x ,y)))

because of the problems we discussed last time.

To help us with problems like this, Common Lisp provides the function get-setf-expansion. To see how it works, let’s call it on the place we used last time, (aref a i):

CL-USER> (get-setf-expansion '(aref a i))
(#:G1409 #:G1410)
(A I)
(#:G1408)
(CCL::ASET #:G1409 #:G1410 #:G1408)
(AREF #:G1409 #:G1410)

The call returns 5 values. The second is a list of input arguments for the aref and the first is a list of corresponding temporaries into which those arguments should be evaluated. The third is a list (always of length 1) of a temporary into which the final value to be stored should be put. The fourth value is code to store the final value and the fifth is code to retrieve the value referenced by the setf argument.

We want our macro to expand into something like

(let* ((#:G1409 A)
       (#:G1410 I)
       (#:1408 (op (aref #:G1409 #:G1410) y)))
  (CCL::ASET #:G1409 #:G1410 #:G1408))

Notice that if the i variable is something like (incf i) this still works because i is evaluated only once.

With that skeleton, it’s pretty easy to write the macro

(defmacro op= (op place y)
  (multiple-value-bind (temps args val setter getter)
      (get-setf-expansion place)
    `(let* ,(append (mapcar #'list temps args)
                    `((,(car val) (,op ,getter ,y))))
       ,setter)))

When we expand it we get

CL-USER> (macroexpand '(op= + (aref a i) 3))
(LET* ((#:G1470 A) (#:G1471 I) (#:G1469 (+ (AREF #:G1470 #:G1471) 3))) (CCL::ASET #:G1470 #:G1471 #:G1469))
T

which is a slightly less well-formatted version of our skeleton. When we try it out, we get the expected result

CL-USER> (let ((a #(2 4 6 8)) (i 2))
           (op= + (aref a i) 3)
           a)
#(2 4 9 8‍)

The idea for the op= macro is based on Paul Graham’s _f macro from On Lisp, a great book for learning about macros.

We’re now in a position to understand define-modify-macro. What it does is to take its arguments and then build a defmacro much like op=‘s. The resulting macro calls get-setf-expansion, sets up a let* just like op=, and then inserts ,setter as the body of the let*.

This entry was posted in Programming and tagged . Bookmark the permalink.