LISP - Macro Writing Style
Lisp - Best Practices for Writing Macros
Section titled “Lisp - Best Practices for Writing Macros”Macros are a powerful tool for meta-programming, but their power comes with responsibility. Writing clean, predictable, and maintainable macros is crucial for a healthy codebase. This tutorial covers the essential best practices for professional macro development.
1. Maintain Hygiene with gensym
Section titled “1. Maintain Hygiene with gensym”Hygiene is the most critical concept in macro writing. An unhygienic macro introduces internal variables that can accidentally clash with variables in the code where the macro is used. This is called variable capture and leads to unpredictable bugs.
The Solution: gensym
Section titled “The Solution: gensym”The gensym function generates a new, unique symbol that is guaranteed not to exist anywhere else in the code. By using gensym for all internal variables within a macro, you can ensure there will never be a name collision.
Example: A Hygienic swap Macro
Section titled “Example: A Hygienic swap Macro”;; A macro to swap the values of two places (variables).(defmacro swap (x y) "Swaps the values of two places X and Y." (let ((temp-var (gensym "TEMP"))) ; Create a unique symbol for the temporary variable. `(let ((,temp-var ,x)) (setf ,x ,y) (setf ,y ,temp-var))))
;; --- Usage ---(let ((a 5) (b 10)) (format t "Before: a=~a, b=~a~%" a b) (swap a b) (format t "After: a=~a, b=~a~%" a b))
;; This works even if a user has a variable named TEMP.(let ((a 1) (temp 99)) (format t "Before: a=~a, temp=~a~%" a temp) (swap a temp) (format t "After: a=~a, temp=~a~%" a temp))2. Use Quasiquotation for Clarity
Section titled “2. Use Quasiquotation for Clarity”Building code by manually constructing lists (e.g., (list 'let ...) ) is verbose and error-prone. Quasiquotation (the backquote syntax) is the standard and most readable way to construct code templates inside a macro.
- Backquote (
): Creates a code template. Most of it is treated as literal data. - Comma (
,): ‘Unquotes’ the following expression, evaluating it and inserting the result into the template. - Comma-At (
,@): ‘Splices’ the following expression. It must evaluate to a list, and the list’s elements are inserted into the template as if they were typed there directly.
3. Debug with macroexpand-1
Section titled “3. Debug with macroexpand-1”When a macro behaves unexpectedly, your most valuable tool is macroexpand-1. This function takes a macro form and shows you the code it expands into, one level deep. This allows you to inspect the generated code and spot errors.
Inspecting the swap Macro
Section titled “Inspecting the swap Macro”;; At the REPL, quote the form you want to inspect.CL-USER> (macroexpand-1 '(swap my-var other-var))
;; The output will look something like this:(LET ((#:TEMP1057 MY-VAR)) (SETF MY-VAR OTHER-VAR) (SETF OTHER-VAR #:TEMP1057))TThe #:TEMP1057 is the printed representation of the uninterned symbol created by gensym. Seeing this confirms our macro is hygienic.
4. Avoid Common Pitfalls
Section titled “4. Avoid Common Pitfalls”Pitfall: Evaluating Arguments Multiple Times
Section titled “Pitfall: Evaluating Arguments Multiple Times”A naive macro might evaluate its arguments more than once, leading to incorrect behavior if the arguments have side effects.
;; BAD macro: `x` is evaluated twice!(defmacro bad-square (x) `(* ,x ,x))
(let ((i 2)) (bad-square (incf i))) ; Expands to (* (incf i) (incf i)), which is 3 * 4 = 12!
;; GOOD macro: evaluate the argument once and store it in a temporary variable.(defmacro good-square (x) (let ((val (gensym))) `(let ((,val ,x)) (* ,val ,val))))
(let ((i 2)) (good-square (incf i))) ; Correctly evaluates to 3 * 3 = 95. Document Everything
Section titled “5. Document Everything”Always include a documentation string in your macros. Explain:
- What the macro does.
- The syntax it introduces.
- Any non-obvious evaluation rules.
- A clear example of its usage.