Macros

Users can create their own special forms by defining macros. A macro is a symbol that has a transformer procedure associated with it. When Scheme encounters a macro-expression — ie, a form whose head is a macro —, it applies the macro’s transformer to the subforms in the macro-expression, and evaluates the result of the transformation.

Ideally, a macro specifies a purely textual transformation from code text to other code text. This kind of transformation is useful for abbreviating an involved and perhaps frequently occurring textual pattern.

A macro is defined using the special form define‑macro (but see sec A.3).1 For example, if your Scheme lacks the conditional special form when, you could define when as the following macro:

(define-macro when
  (lambda (test . branch)
    (list 'if test
      (cons 'begin branch))))

This defines a when-transformer that would convert a when-expression into the equivalent if-expression. With this macro definition in place, the when-expression

(when (< (pressure tube) 60)
   (open-valve tube)
   (attach floor-pump tube)
   (depress floor-pump 5)
   (detach floor-pump tube)
   (close-valve tube))

will be converted to another expression, the result of applying the when-transformer to the when-expression’s subforms:

(apply
  (lambda (test . branch)
    (list 'if test
      (cons 'begin branch)))
  '((< (pressure tube) 60)
      (open-valve tube)
      (attach floor-pump tube)
      (depress floor-pump 5)
      (detach floor-pump tube)
      (close-valve tube)))

The transformation yields the list

(if (< (pressure tube) 60)
    (begin
      (open-valve tube)
      (attach floor-pump tube)
      (depress floor-pump 5)
      (detach floor-pump tube)
      (close-valve tube)))

Scheme will then evaluate this expression, as it would any other.

As an additional example, here is the macro-definition for when’s counterpart unless:

(define-macro unless
  (lambda (test . branch)
    (list 'if
          (list 'not test)
          (cons 'begin branch))))

Alternatively, we could invoke when inside unless’s definition:

(define-macro unless
  (lambda (test . branch)
    (cons 'when
          (cons (list 'not test) branch))))

Macro expansions can refer to other macros.

8.1  Specifying the expansion as a template

A macro transformer takes some s-expressions and produces an s-expression that will be used as a form. Typically this output is a list. In our when example, the output list is created using

(list 'if test
  (cons 'begin branch))

where test is bound to the macro’s first subform, ie,

(< (pressure tube) 60)

and branch to the rest of the macro’s subforms, ie,

((open-valve tube)
 (attach floor-pump tube)
 (depress floor-pump 5)
 (detach floor-pump tube)
 (close-valve tube))

Output lists can be quite complicated. It is easy to see that a more ambitious macro than when could lead to quite an elaborate construction process for the output list. In such cases, it is more convenient to specify the macro’s output form as a template, with the macro arguments inserted at appropriate places to fill out the template for each particular use of the macro. Scheme provides the backquote syntax to specify such templates. Thus the expression

(list 'IF test
  (cons 'BEGIN branch))

is more conveniently written as

`(IF ,test
  (BEGIN ,@branch))

We can refashion the when macro-definition as:

(define-macro when
  (lambda (test . branch)
    `(IF ,test
         (BEGIN ,@branch))))

Note that the template format, unlike the earlier list construction, gives immediate visual indication of the shape of the output list. The backquote (`) introduces a template for a list. The elements of the template appear verbatim in the resulting list, except when they are prefixed by a comma (‘,’) or a comma-splice (‘,@’). (For the purpose of illustration, we have written the verbatim elements of the template in UPPER-CASE.)

The comma and the comma-splice are used to insert the macro arguments into the template. The comma inserts the result of evaluating its following expression. The comma-splice inserts the result of evaluating its following expression after splicing it, ie, it removes the outermost set of parentheses. (This implies that an expression introduced by comma-splice must be a list.)

In our example, given the values that test and branch are bound to, it is easy to see that the template will expand to the required

(IF (< (pressure tube) 60)
    (BEGIN
      (open-valve tube)
      (attach floor-pump tube)
      (depress floor-pump 5)
      (detach floor-pump tube)
      (close-valve tube)))

8.2  Avoiding variable capture inside macros

A two-argument disjunction form, my‑or, could be defined as follows:

(define-macro my-or
  (lambda (x y)
    `(if ,x ,x ,y)))

my‑or takes two arguments and returns the value of the first of them that is true (ie, non-#f). In particular, the second argument is evaluated only if the first turns out to be false.

(my-or 1 2)
=>  1

(my-or #f 2)
=>  2

There is a problem with the my‑or macro as it is written. It re-evaluates the first argument if it is true: once in the if-test, and once again in the “then” branch. This can cause undesired behavior if the first argument were to contain side-effects, eg,

(my-or
  (begin 
    (display "doing first argument")
     (newline)
     #t)
  2)

displays "doing first argument" twice.

This can be avoided by storing the if-test result in a local variable:

(define-macro my-or
  (lambda (x y)
    `(let ((temp ,x))
       (if temp temp ,y))))

This is almost OK, except in the case where the second argument happens to contain the same identifier temp as used in the macro definition. Eg,

(define temp 3)

(my-or #f temp)
=>  #f

Surely it should be 3! The fiasco happens because the macro uses a local variable temp to store the value of the first argument (#f) and the variable temp in the second argument got captured by the temp introduced by the macro.

To avoid this, we need to be careful in choosing local variables inside macro definitions. We could choose outlandish names for such variables and hope fervently that nobody else comes up with them. Eg,

(define-macro my-or
  (lambda (x y)
    `(let ((+temp ,x))
       (if +temp +temp ,y))))

This will work given the tacit understanding that +temp will not be used by code outside the macro. This is of course an understanding waiting to be disillusioned.

A more reliable, if verbose, approach is to use generated symbols that are guaranteed not to be obtainable by other means. The procedure gensym generates unique symbols each time it is called. Here is a safe definition for my‑or using gensym:

(define-macro my-or
  (lambda (x y)
    (let ((temp (gensym)))
      `(let ((,temp ,x))
         (if ,temp ,temp ,y)))))

In the macros defined in this document, in order to be concise, we will not use the gensym approach. Instead, we will consider the point about variable capture as having been made, and go ahead with the less cluttered +-as-prefix approach. We will leave it to the astute reader to remember to convert these +-identifiers into gensyms in the manner outlined above.

8.3  fluid‑let

Here is a definition of a rather more complicated macro, fluid‑let (sec 5.2). fluid‑let specifies temporary bindings for a set of already existing lexical variables. Given a fluid‑let expression such as

(fluid-let ((x 9) (y (+ y 1)))
  (+ x y))

we want the expansion to be

(let ((OLD-X x) (OLD-Y y))
  (set! x 9)
  (set! y (+ y 1))
  (let ((RESULT (begin (+ x y))))
    (set! x OLD-X)
    (set! y OLD-Y)
    RESULT))

where we want the identifiers OLD‑X, OLD‑Y, and RESULT to be symbols that will not capture variables in the expressions in the fluid‑let form.

Here is how we go about fashioning a fluid‑let macro that implements what we want:

(define-macro fluid-let
  (lambda (xexe . body)
    (let ((xx (map car xexe))
          (ee (map cadr xexe))
          (old-xx (map (lambda (ig) (gensym)) xexe))
          (result (gensym)))
      `(let ,(map (lambda (old-x x) `(,old-x ,x)) 
                  old-xx xx)
         ,@(map (lambda (x e)
                  `(set! ,x ,e)) 
                xx ee)
         (let ((,result (begin ,@body)))
           ,@(map (lambda (x old-x)
                    `(set! ,x ,old-x)) 
                  xx old-xx)
           ,result)))))

The macro’s arguments are: xexe, the list of variable/expression pairs introduced by the fluid‑let; and body, the list of expressions in the body of the fluid‑let. In our example, these are ((x 9) (y (+ y 1))) and ((+ x y)) respectively.

The macro body introduces a bunch of local variables: xx is the list of the variables extracted from the variable/expression pairs. ee is the corresponding list of expressions. old‑xx is a list of fresh identifiers, one for each variable in xx. These are used to store the incoming values of the xx, so we can revert the xx back to them once the fluid‑let body has been evaluated. result is another fresh identifier, used to store the value of the fluid‑let body. In our example, xx is (x y) and ee is (9 (+ y 1)). Depending on how your system implements gensym, old‑xx might be the list (GEN‑63 GEN‑64), and result might be GEN‑65.

The output list is created by the macro for our given example looks like

(let ((GEN-63 x) (GEN-64 y))
  (set! x 9)
  (set! y (+ y 1))
  (let ((GEN-65 (begin (+ x y))))
    (set! x GEN-63)
    (set! y GEN-64)
    GEN-65))

which matches our requirement.


1 MzScheme provides define‑macro via the defmacro library. Use (require (lib "defmacro.ss")) to load this library.