Search code examples
macroslispcommon-lisplexical-scope

Lexical Bindings in Common Lisp Macros


I am currently working my way through Graham's On Lisp and find this particular bit difficult to understand:

Binding. Lexical variables must appear directly in the source code. The first argument to setq is not evaluated, for example, so anything built on setq must be a macro which expands into a setq, rather than a function which calls it. Likewise for operators like let, whose arguments are to appear as parameters in a lambda expression, for macros like do which expand into lets, and so on. Any new operator which is to alter the lexical bindings of its arguments must be written as a macro.

This comes from Chapter 8, which describes when macros should and should not be used in place of functions.

What exactly does he mean in this paragraph? Could someone give a concrete example or two?

Much appreciated!


Solution

  • setq is a special form and does not evaluate its first argument. Thus if you want to make a macro that updates something, you cannot do it like this:

    (defun update (what with)
      (setq what with))
    
    (defparameter *test* 10)
    (update *test* 20)        ; what does it do?
    *test*                    ; ==> 10
    

    So inside the function update setq updates the variable what to be 20, but it is a local variable that has the value 10 that gets updated, not *test* itself. In order to update *test* setq must have *test* as first argument. A macro can do that:

    (defmacro update (what with)
      `(setq ,what ,with))
    
    (update *test* 20)        ; what does it do?
    *test*                    ; ==> 20
    

    You can see exactly the resulting code form the macro expansion:

    (macroexpand-1 '(update *test* 20))
    ; ==> (setq *test* 20) ; t
    

    A similar example. You cannot mimic if with cond using a function:

    (defun my-if (test then else)
      (cond (test then)
            (t else)))
    
    (defun fib (n)
      (my-if (< 2 n) 
             n
             (+ (fib (- n 1)) (fib (- n 2)))))
    
    (fib 3)
    

    No matter what argument you pass you get an infinite loop that always call the recursive case since all my-if arguments are always evaluated. With cond and if the test gets evaluated and based on that either the then or else is evaluated, but never all unconditionally.