Thursday, 8 November 2007

Common Lisp macros cheatsheet

One great feature of Lisp is that it's syntax is very minimal. When it comes to macros, the syntax really is (syntactically) minimal. A macro is basically Lisp code that gets run at "compile time" and is usually used to generate code. However, since it is Lisp code, it could just as well access a database if you are into those sort of things! Take a look at yesterday's post for a useful example of code generation with macros.

Since macros are just Lisp code, there are special characters that come into play when evaluating macros, essentially to determine when the code will be evaluated. By default, the code is evaluated at macroexpand-time.

; this progn is executed at macroexpand time
CL-USER> (defmacro test-1 () (progn 'foo))
CL-USER> (macroexpand '(test-1))
; this progn is executed at runtime
CL-USER> (defmacro test-2 () `(progn 'foo))
CL-USER> (macroexpand '(test-2))

So the `(backquote) character delays evaluation until runtime. Nice. Here we've used the quite helpful macroexpand function to expand the macro.

What if you wanted to write a macro that would print the sum of it's two constant arguments? You might try writing:

CL-USER> (defmacro test-3 (a b) (print (+ a b)))

But watch what happens when we expand it:

CL-USER> (macroexpand-1 '(test-3 1 2))

Oops! What we really wanted to happen was have (test-3 1 2) expand into (print 3) but what our attempt above really did was print out the result at macroexpand time. To get around this, we want to be explicit about what to evaluate during macroexpand time and what to leave until runtime:

CL-USER> (defmacro test-4 (a b) `(print ,(+ a b)))
CL-USER> (macroexpand-1 '(test-4 1 2))

So what we did above was use the backquote character to delay evaluation of the print form but evaluated the sum form by prefixing it with a comma. Double nice.

That leaves only one(?) remaining macro-specific character: ,@. This one is not so complicated given our understanding of the comma character above. What it means is to evaluate the list following ,@ at macroexpand time but removes the outermost level of parentheses and inserts the result at this point. For example:

CL-USER> (defmacro test-5 (operands) `(+ ,operands))
CL-USER> (macroexpand '(test-5 (1 2 3 4 5 6)))
(+ (1 2 3 4 5 6))
CL-USER> (defmacro test-6 (operands) `(+ ,@operands))
CL-USER> (macroexpand '(test-6 (1 2 3 4 5 6)))
(+ 1 2 3 4 5 6)

So to summarize (i.e., cheatsheet!):

  • ` - backquote operator: delay evaluation of prefixed form until runtime

  • , - comma operator: within a delayed form (i.e., backquoted), evaluate prefixed form at macroexpand time

  • ,@ - comma-at operator: within a delayed form (i.e., backquoted), evaluate prefixed form at macroexpand time and remove outermost level of parentheses

  • macroexpand is a useful function for debugging the code your macros generate

How does one go back to C++/Java and friends after this?

Update: See this followup post to see what a Lisp noob I really am.

No comments: