First are macros that circumvent the regular call-by-value semantics. These might evaluate a subform at macro expansion time, treat a subform as a place (an l-value) rather than a value, or otherwise treat a subform as something other than a runtime function call. For example, if
incffully evaluated its argument, it could perform the increment on the value, but it couldn't put the value back where it got it. Another example is the
check-typemacro. You use it like this:
(defun foo (x) (check-type foo (integer 1 *) "a positive integer") (bar (- x 1)))The
check-typemacro has to be a macro because it treats
fooas a place (it will allow you to proceed by modifying
foo), and it treats its second argument as a type specifier.
Second are macros that introduce new syntax to the language. Examples are
defvar, etc. These treat their arguments specially or have special clauses that don't act like ordinary function calls.
CL-USER> (macroexpand-all '(do ((i 0 (+ i 1)) (j 1 (* j 2))) ((> j 65536) nil) (format t "~%~2d ~5d" i j))) (BLOCK NIL (COMMON-LISP:LET ((I 0) (J 1)) (TAGBODY (GO #:G748) #:G747 (TAGBODY (FORMAT T "~%~2d ~5d" I J)) (COMMON-LISP:LET* ((#:NEW1 (+ I 1)) (#:NEW1 (* J 2))) (SETQ I #:NEW1) (SETQ J #:NEW1) NIL) #:G748 (IF (> J 65536) NIL (GO #:G747)) (RETURN-FROM NIL (PROGN NIL))))
Third are macros that implement tiny languages within Common Lisp. The
loopmacro is a good example. It looks like this
(loop for i from 1 to (compute-top-value) while (not (unacceptable i)) collect (square i) do (format t "Working on ~D now" i) when (evenp i) do (format t "~D is a non-odd number" i) finally (format t "About to exit!"))It works like a little compiler. It parses the loop clauses and generates a Lisp form that carries them out
(BLOCK NIL (LET ((I 1) (#:LOOP-LIMIT-744 (COMPUTE-TOP-VALUE))) (DECLARE (TYPE (AND NUMBER REAL) #:LOOP-LIMIT-744) (TYPE (AND REAL NUMBER) I)) (SB-LOOP::WITH-LOOP-LIST-COLLECTION-HEAD (#:LOOP-LIST-HEAD-745 #:LOOP-LIST-TAIL-746) (TAGBODY SB-LOOP::NEXT-LOOP (WHEN (> I #:LOOP-LIMIT-744) (GO SB-LOOP::END-LOOP)) (UNLESS (NOT (UNACCEPTABLE I)) (GO SB-LOOP::END-LOOP)) (SB-LOOP::LOOP-COLLECT-RPLACD (#:LOOP-LIST-HEAD-745 #:LOOP-LIST-TAIL-746) (LIST (SQUARE I))) (FORMAT T "Working on ~D now" I) (IF (EVENP I) (FORMAT T "~D is a non-odd number" I)) (SB-LOOP::LOOP-DESETQ I (1+ I)) (GO SB-LOOP::NEXT-LOOP) SB-LOOP::END-LOOP (FORMAT T "About to exit!") (RETURN-FROM NIL (SB-LOOP::LOOP-COLLECT-ANSWER #:LOOP-LIST-HEAD-745)))))
Fourth are macros that run code in a special context. The
unlessfall in this category. These macros take ordinary Lisp code and wrap it with other code that establishes a context or tests a conditional.
CL-USER> (macroexpand '(when (condition) (foo) (bar))) (IF (CONDITION) (PROGN (FOO) (BAR))) CL-USER> (macroexpand '(with-open-file (foo "~/.bashrc" :if-does-not-exist :create) (print (read-line foo)) (bar))) (LET ((FOO (OPEN "~/.bashrc" :IF-DOES-NOT-EXIST :CREATE)) (#:G751 T)) (UNWIND-PROTECT (MULTIPLE-VALUE-PROG1 (PROGN (PRINT (READ-LINE FOO)) (BAR)) (SETQ #:G751 NIL)) (WHEN FOO (CLOSE FOO :ABORT #:G751))))
These aren't hard and fast categories, many macros can be thought of as in more than one category. All macros work by syntactic transformation and most treat at least one of their subforms as something other than a call-by-value form, for instance. There are also the occasional macros that have the look and feel of a standard function calls. The series package appears to allow you to manipulate series through standard function calls, but works by clever macroexpansion into iterative code.
I find it useful to think of macros in these four different ways, but I'm sure that others have their own categorizations that they find useful.
AddendumAn anonymous reader asked, “What about code walking/analysis/optimization?”. I really overlooked that. I think Richard Waters's series package would be a good example. It takes ordinary functional programs that can operate on series of data (coinductively defined data or codata), and turns it into the equivalent iterative construct that operates on one element at a time. It does this by clever macros that walk the code, analyse it, and rewrite it to a more optimal form
CL-USER> (sb-cltl2:macroexpand-all '(let ((x (scan-range :from 0 :below 10))) (collect (choose-if #'evenp x)))) (COMMON-LISP:LET* ((X (COERCE (- 0 1) 'NUMBER)) (#:LASTCONS-751 (LIST NIL)) (#:LST-752 #:LASTCONS-751)) (DECLARE (TYPE NUMBER X) (TYPE CONS #:LASTCONS-751) (TYPE LIST #:LST-752)) (TAGBODY #:LL-756 (SETQ X (+ X (COERCE 1 'NUMBER))) (IF (NOT (< X 10)) (GO SERIES::END)) (IF (NOT (EVENP X)) (GO #:LL-756)) (SETQ #:LASTCONS-751 (SB-KERNEL:%RPLACD #:LASTCONS-751 (CONS X NIL))) (GO #:LL-756) SERIES::END) (CDR #:LST-752)))As you can see, the series code does a major rewrite of the original lisp code. An astute reader will notice that the
letform has to have been redefined to do dataflow analysis of it's bindings and body. Thanks to anonymous for the suggestion.
Addendum 2: Symbol macrosA comment by Paul F. Dietz got me thinking about symbols and it occurred to me that symbol macros deserve their own category as well. Symbol macros appear to be an ordinary symbolic references to variables, but they expand to some code that computes a value. For instance, if
foowere a symbol macro that expanded to
(car bar), then using it in a form such as
(+ foo 7)would expand to
(+ (car bar) 7). A symbol macro is a two-edged sword. It is a very useful abstraction for providing a name to a computed value, but they also can fool the user of such a macro into thinking that a simple variable reference is happening when some more complex computation could be happening.
I think that makes seven ways and counting.