LISP - Scope and Binding
Lisp - Macro Hygienics: Scope and Bindings
Section titled “Lisp - Macro Hygienics: Scope and Bindings”Macros are powerful code-transformation tools, but this power comes with a critical responsibility: managing variable scope. Because macros inject code directly into their call site, they can accidentally interfere with existing variables. This chapter explains the problem of variable capture and how to write clean, predictable, hygienic macros in Common Lisp.
The Problem: Unhygienic Macros and Variable Capture
Section titled “The Problem: Unhygienic Macros and Variable Capture”A macro is unhygienic if it introduces bindings that can accidentally capture or clash with variables in the surrounding code. This happens when a symbol used inside the macro is the same as a symbol at the call site.
Example: A Flawed swap Macro
Section titled “Example: A Flawed swap Macro”Let’s define a macro to swap the values of two places. A naive implementation might use a temporary variable named temp.
;; Flawed macro: uses a fixed symbol `temp` for its internal variable.(defmacro unhygienic-swap (a b) `(let ((temp ,a)) (setf ,a ,b) (setf ,b temp)))This works fine in most cases. However, it fails spectacularly if one of the variables we want to swap is also named temp.
(let ((temp 10) (my-var 20)) (format t "Before: temp=~d, my-var=~d~%" temp my-var) (unhygienic-swap temp my-var) (format t "After: temp=~d, my-var=~d~%" temp my-var))Output
Section titled “Output”Before: temp=10, my-var=20After: temp=20, my-var=20The swap failed! To see why, we can use macroexpand-1 to view the generated code:
;; CL-USER> (macroexpand-1 '(unhygienic-swap temp my-var))
(LET ((TEMP TEMP)) (SETF TEMP MY-VAR) (SETF MY-VAR TEMP))The (let ((temp temp)) binding shadows the outer temp, causing my-var to be set to its own new value instead of the original value of temp.
The Solution: gensym for Hygienic Macros
Section titled “The Solution: gensym for Hygienic Macros”The solution is to use symbols that are guaranteed to be unique. The gensym function creates a new, uninterned symbol on each call, which cannot clash with any existing symbols.
Example: A Correct, Hygienic swap Macro
Section titled “Example: A Correct, Hygienic swap Macro”;; Correct macro: uses a generated symbol for its internal variable.(defmacro hygienic-swap (a b) (let ((temp-sym (gensym))) `(let ((,temp-sym ,a)) (setf ,a ,b) (setf ,b ,temp-sym))))Now, let’s try the same test case again:
(let ((temp 10) (my-var 20)) (format t "Before: temp=~d, my-var=~d~%" temp my-var) (hygienic-swap temp my-var) (format t "After: temp=~d, my-var=~d~%" temp my-var))Output
Section titled “Output”Before: temp=10, my-var=20After: temp=20, my-var=10It works correctly! The expansion now uses a unique symbol (e.g., #:G1234) that doesn’t conflict with our temp variable.
Local Macros with macrolet
Section titled “Local Macros with macrolet”The macrolet special operator allows you to define local macros that are only available within a specific lexical scope. This is useful for creating syntactic sugar or a small domain-specific language (DSL) inside a function, without polluting the global namespace.
(defun calculate-trajectory (x) ;; Define a local macro 'square' available only inside this function. (macrolet ((square (n) `(* ,n ,n))) ;; Use the local macro. (+ (square x) (square (+ x 1)))))
(print (calculate-trajectory 5))Output
Section titled “Output”61Best Practices for Writing Macros
Section titled “Best Practices for Writing Macros”- Always use
gensymfor any temporary variables introduced within your macro expansion. - Avoid multiple evaluations of macro arguments. If an argument might have side effects (e.g.,
(incf i)), store its result in agensym’d variable and use that variable in the expansion.