Macros

Nyquist / XLISP 2.0 - Contents | Tutorials | Examples | Reference

# Macro Programming

- with-unique-names - create local gensym variables

Note: The best book for Lisp macro programming is Paul Graham's 'On Lisp', available for free under:

- http://www.paulgraham.com/onlisp.html

## with-unique-names

See http://www.cliki.net/WITH-UNIQUE-NAMES. This macro also appears in Chapter 11 of Paul Graham's On Lisp under the name 'with-gensyms'.

(with-unique-names (symbols) body)
  symbols - a list of Lisp symbols, representing variable names
  body - some Lisp code to execute
  returns - the body with all symbols bound to different gensyms

The 'with-unique-names' macro helps to avoid name clashes in Lisp macros.

    (defmacro with-unique-names (symbols &rest body)
      `(let ,(mapcar #'(lambda (x) `(,x (gensym))) symbols) ,@body))

The 'with-unique-names' macro belongs to the category of write-only code. No matter how you write it, it's nearly impossible to understand its meaning by reading the macro definition. It's easier to understand if you look at the macro expansion:

    > (macroexpand-1 '(with-unique-names (a b c)
                        `(let ((,a 1) (,b 2) (,c 3))
                           (list ,a ,b ,c))))

    (let ((a (gensym)) (b (gensym)) (c (gensym)))
      `(let ((,a 1) (,b 2) (,c 3))
         (list ,a ,b ,c)))

This translates in practice to the following idea:

    (let ((a (gensym)) (b (gensym)) (c (gensym)))  ; outside the expansion
      `(let ((gensym1 1) (gensym2 2) (gensym3 3))  ; inside the expansion
         (list gensym1 gensym2 gensym3)))

The variable names 'a', 'b', and 'c' have been replaced inside the macro expansion by three gensyms. This way a variable name inside the macro expansion cannot accidentally collide with a variable of the same name in the environment of the macro's expansion like shown here:

    (defmacro print-macro (x)         ; bad example
      `(let ((macro-var 'macro))
         (print ,x)))

    > (let ((local-var 'let))         ; this works
        (print local-var)
        (print-macro local-var))
    LET  ; printed by PRINT
    LET  ; printed by PRINT-MACRO

    > (let ((macro-var 'let))         ; this doesn't
        (print macro-var)
        (print-macro macro-var))
    LET    ; printed by PRINT
    MACRO  ; printed by PRINT-MACRO

The reason for this behaviour is that the 'print-macro' expands to:

    > (let ((local-var 'let))         ; this works
        (print local-var)
        (let ((macro-var 'macro))
          (print local-var)))
    LET  ; LOCAL-VAR inside the first LET
    LET  ; LOCAL-VAR inside the second LET

    > (let ((macro-var 'let))         ; this doesn't
        (print macro-var)
        (let ((macro-var 'macro))
          (print macro-var)))
    LET    ; MACRO-VAR inside the first LET
    MACRO  ; MACRO-VAR inside the second LET

Now the same example with unique names. Note the comma before the 'macro-var' inside the let form of the macro definition:

    (defmacro print-macro (x)         ; good example
      (with-unique-names (macro-var)
        `(let ((,macro-var 'macro))
           (print ,x))))

    > (let ((macro-var 'let))         ; now it works
        (print macro-var)
        (print-macro macro-var))
    LET  ; printed by PRINT
    LET  ; printed by PRINT-MACRO

The reason why it works is that the 'print-macro' now expands to:

    > (let ((macro-var 'let))         ; works
        (print macro-var)
        (let ((gensym 'macro))
          (print macro-var)))
    LET  ; MACRO-VAR inside the first LET
    LET  ; MACRO-VAR inside the second LET

Now 'macro-var' can even be used as a variable name inside the macro definition without colliding with the 'macro-var' bound by let:

    (defmacro print-macro (x)         ; good example
      (with-unique-names (macro-var)
        `(let ((,macro-var 'macro))
           (print ,macro-var)
           (print ,x))))

    > (let ((macro-var 'let))         ; works
        (print macro-var)
        (print-macro macro-var))
    LET     ; MACRO-VAR printed inside LET
    MACRO   ; GENSYMed MACRO-VAR, printed inside PRINT-MACRO
    LET     ; MACRO-VAR bound by LET, printed inside PRINT-MACRO

The expansion of the 'print-macro' shows why this works:

    > (let ((macro-var 'let))         ; works
        (print macro-var)
        (let ((gensym 'macro))
          (print gensym)
          (print macro-var)))
    LET     ; MACRO-VAR printed inside LET
    MACRO   ; GENSYMed MACRO-VAR printed inside PRINT-MACRO
    LET     ; MACRO-VAR bound by LET, printed inside PRINT-MACRO

You can give as many variable names as you like to 'with-unique-names', the gensym management is done automatically:

    (defmacro print-macro (x y z)
      (with-unique-names (a b c)
        `(let ((,a 1) (,b 2) (,c 3))
           (format t "outside: a: ~a  b: ~a  c: ~a~%" ,x ,y ,z)
           (format t " inside: a: ~a  b: ~a  c: ~a~%" ,a ,b ,c))))

    > (let ((a 'a) (b 'b) (c 'c))
        (print-macro a b c))
    outside: a: A  b: B  c: C
     inside: a: 1  b: 2  c: 3

Two things you still have to care about:

1. The 'unique names' should not use the same smbol names as the parameter variables of the macro, otherwise you will have the same 'shadowing' effect like in ordinary Lisp functions. This is not a real problem because when writing a macro you can see the parameter names before your eyes, while you usually cannot see the variable names of the environment, where the macro will be expanded. You also do not have to care which variable names had been used in a macro if you call the macro from arbitrary Lisp code, where you usually cannot see the code of the macro definition.

2. The local gensymed variables now themselves must be expanded by writing a comma in front of each when they appear inside a backquote scope. This sometimes can lead to tricky situations, because the comma expansion of the symbol does not produce the variable's value, instead it produces the name of the gensym, which holds the value. But this is a general phenomenon of gensyms in Lisp macro programming and not a bug of the 'with-unique-names' macro.

The alternative would be writing:

    (defmacro print-macro (x y z)
      (let ((a (gensym)) (b (gensym)) (c (gensym)))
        `(let ((,a 1) (,b 2) (,c 3))
           (format t "outside: a: ~a  b: ~a  c: ~a~%" ,x ,y ,z)
           (format t " inside: a: ~a  b: ~a  c: ~a~%" ,a ,b ,c))))

Back to top

Nyquist / XLISP 2.0 - Contents | Tutorials | Examples | Reference