LISP - Code Generation Using Macro
Lisp - Code Generation with Macros
Section titled “Lisp - Code Generation with Macros”In Lisp, macros are a powerful tool for metaprogramming—writing code that writes code. Unlike functions, which evaluate their arguments before execution, macros are given the unevaluated Lisp code of their arguments. This allows you to transform that code before it’s compiled and run, effectively extending the Lisp language with new syntax and control structures. In this chapter, we’ll explore how to build our own language features using macros.
Macros vs. Functions: The Core Difference
Section titled “Macros vs. Functions: The Core Difference”The key to understanding macros is knowing that they operate on code, while functions operate on data (the results of evaluated code). Let’s see why this matters.
Consider a square function:
(defun square-fn (n) (* n n))If you call it with (square-fn (+ 2 3)), the (+ 2 3) is evaluated first, becoming 5, and then (square-fn 5) is called. This is usually what you want.
But what if the argument has a side effect?
(let ((x 1)) (square-fn (incf x))) ; incf increments x and returns the new valueHere, (incf x) runs once, x becomes 2, and (square-fn 2) is called, returning 4. The side effect happens only once. A macro behaves differently, which can lead to unexpected behavior if not written carefully.
Example: A Simple square Macro
Section titled “Example: A Simple square Macro”Let’s define a macro to generate the code for squaring a number. We use defmacro and the backquote ( `) syntax for templating.
main.lisp
Section titled “main.lisp”;; A macro to generate code that squares an expression.(defmacro my-square (expr) "Expands to code that multiplies the expression by itself." `(* ,expr ,expr))
;; --- Usage ---
;; Use the built-in macroexpand-1 to see what the macro generates.(print (macroexpand-1 '(my-square 5)))(terpri)
;; Normal execution(print (my-square 5))(terpri)
;; What happens with a side effect?(let ((counter 0)) (print (my-square (incf counter))) ; Expands to (* (incf counter) (incf counter)) (print counter)) ; The counter was incremented TWICE!Output
Section titled “Output”(* 5 5)T
25
42Explanation
Section titled “Explanation”defmacro my-square (expr): Defines a macro namedmy-squarethat takes one argument,expr. Importantly,expris the unevaluated code passed to the macro, not its value.- Backquote (
`): The backquote starts a template. Everything inside is treated as literal data, not code to be executed immediately. - Comma (
,): The commaunquotesan item, telling Lisp to evaluate it and splice its value into the template. Here,,exprinserts the code tree for5or(+ 2 3). The double-evaluation pitfall withincfis a classic macro problem. macroexpand-1: This is a crucial debugging tool. It shows you the code your macro generates in one step, allowing you to verify its output before running it.
Example: A Hygienic square Macro with Gensym
Section titled “Example: A Hygienic square Macro with Gensym”To fix the multiple evaluation problem, we need to ensure the expression is evaluated only once. We can do this by creating a temporary variable inside the macro expansion. To avoid accidentally conflicting with variables in the surrounding code, we must use gensym to create a unique, un-interned symbol.
main.lisp
Section titled “main.lisp”;; A better, safer square macro.(defmacro my-safe-square (expr) "Safely squares an expression by evaluating it only once." (let ((temp-var (gensym))) `(let ((,temp-var ,expr)) (* ,temp-var ,temp-var))))
;; --- Usage ---
(print (macroexpand-1 '(my-safe-square (incf counter))))(terpri)
(let ((counter 0)) (print (my-safe-square (incf counter))) (print counter)); The counter is now incremented only once!Output
Section titled “Output”(LET ((#<GENSYM> (INCF COUNTER))) (* #<GENSYM> #<GENSYM>))T
11Explanation
Section titled “Explanation”- Hygiene: This macro is considered “hygienic” because it avoids unintended variable capture and repeated evaluation.
gensym: Generates a new, unique symbol (like#<GENSYM>) that cannot possibly clash with any existing variable name.let: Theletform inside the macro expansion creates a temporary variable. The result of evaluatingexpris stored in this variable, which is then used twice.
Example: Creating a Custom Loop
Section titled “Example: Creating a Custom Loop”Macros shine when creating new control structures. Here’s a simple while loop, a structure not built into Common Lisp.
main.lisp
Section titled “main.lisp”;; A macro for a classic 'while' loop.(defmacro my-while (condition &body body) "Executes body expressions as long as condition is true." `(loop while ,condition do (progn ,@body)))
;; --- Usage ---
(let ((i 0)) (my-while (< i 5) (print i) (incf i)))Output
Section titled “Output”01234Explanation
Section titled “Explanation”&body: This is a special lambda-list keyword that collects all remaining forms into a list, perfect for the body of a loop or function-like structure.- Comma-At (
,@): The comma-at operator splices a list of forms into the template. Ifbodyis((print i) (incf i)), then,@bodyinserts(print i) (incf i)directly into theprognform.prognis used to group multiple expressions into a single block.
Best Practices and Debugging
Section titled “Best Practices and Debugging”- When to use a macro?: Only when a function won’t work. Use a macro if you need to control evaluation, create new syntax, or perform compile-time computations.
- Keep them simple: Complex macros are hard to debug. If a macro gets too complicated, consider using helper functions within the macro definition.
- Always use
gensym: For any temporary variables in your macro expansion, usegensymto avoid bugs. - Debug with
macroexpand-1: Always check the code your macro generates. It’s the most effective way to find bugs.