Appendix A: Macros
So far we’ve been writing definitions for procedures and interacted with them, and we also interacted with some syntax, like quote, define, and so on. What if we had a way to write our own syntax? For example, could we write syntax that, when executed, would write code itself? Macros are a way to do exactly that.
There is special syntax in Lisp, called define-macro, which allows us to create a macro. It accepts the name of a macro and its arguments (which are optional), and will return a quoted list of Lisp commands.
When we run our definitions (or compile them), Racket will replace all occurrences of the macro call with the actual output that it produces.
As an example, if we have the following macro (define-macro (add-one x) (list '+ x 1)), wherever we write (add-one x), it will be replaced with the expression (+ x 1).
Macros are all about affecting how the evaluation model works. Racket has an eager evaluation model, which means that all expressions will be evaluated at a given time on a procedure call. For macros, on the other hand, since Racket will replace the code before evaluating it, expressions will be evaluated only when needed.
The following example illustrates the difference between a macro and a procedure. Consider the following definitions:
1 (require compatibility/defmacro)
2
3 (define-macro (our-if-macro a b c)
4 (list 'cond (list a b) (list 'else c)))
5
6 (define (our-if-procedure a b c)
7 (cond (a b) (else c)))
With a few evaluations:
1 > (our-if-macro (eq? '() '()) #t #f)
2 #t
3 > (our-if-procedure (eq? '() '()) #t #f)
4 #t
5 > (our-if-macro (eq? '() '()) (display "True") (display "False"))
6 True
7 > (our-if-procedure (eq? '() '()) (display "True") (display "False"))
8 TrueFalse
We can notice a couple of things from this code:
We required a library that contains the define-macro syntax
.
We implemented our own if both as a macro and as a procedure.
In the interactions area, we used a procedure called display, which prints stuff to the output.
We see that the macro and the procedure produce two different outputs.
The macro and the procedure behave differently, as expected. However, in the case of if, it makes sense to implement it as a macro rather than as a procedure. Since Racket has an eager evaluation model, it will evaluate all arguments, but it does not make sense to evaluate the else case if we are sure that the first case will match.
Let’s manually expand the macro to get a feel for how it really works.
1 (our-if-macro (eq? '() '()) #t #f)
2 = (list 'cond (list (eq? '() '()) #t) (list 'else #f))
3 = (list 'cond (list #t #t) (list 'else #f))
4 = '(cond (#t #t) (else #f))
That is, the macro returns a quoted expression that’s supposed to be evaluated once the macro has been used.
The way we’ve written our macro makes things much more explicit in terms of execution and substitution. We can also write it as follows, which is a bit more implicit, where the backtick is like a quote mark (quasiquote) and the comma is like an unquote mark (the opposite of quote):
1 (define-macro (our-if-macro a b c)
2 `(cond (,a ,b) (else ,c)))
Hygienic Macros
The way we wrote the macros earlier is not good practice in Racket. Consider the following example:
1 (define-macro (swap a b)
2 `(let ((tmp ,a))
3 (set! ,a ,b)
4 (set! ,b tmp)))
This looks like a perfectly safe macro. Indeed:
1 > (define x 1)
2 > (define y 2)
3 > (displayln (list x y))
4 (1 2)
5 > (swap x y)
6 > (displayln (list x y))
7 (2 1)
However, we can break it with the following code:
1 > (define x 1)
2 > (define tmp 2)
3 > (displayln (list x tmp))
4 (1 2)
5 > (swap x tmp)
6 > (displayln (list x tmp))
7 (1 2)
To see what happened, we can expand the macro by hand. The macro translates to:
1 > `(let ((tmp ,x))
2 (set! ,x ,tmp)
3 (set! ,tmp tmp))
4 '(let ((tmp 2)) (set! 2 2) (set! 2 tmp))
In this case,
tmp was already defined (using
define) and was reused in the body of the
let. As we can see, it is hard to control the accidental capture of local identifiers. We can get around the problem by using
gensym
, which returns a unique symbol every time it’s called:
1 (define-macro (swap a b)
2 (let ((tmp (gensym)))
3 `(let ((,tmp ,a))
4 (set! ,a ,b)
5 (set! ,b ,tmp))))
Now our macro works as intended:
1 > (define x 1)
2 > (define tmp 2)
3 > (displayln (list x tmp))
4 (1 2)
5 > (swap x tmp)
6 > (displayln (list x tmp))
7 (1 2)
However, instead of
gensym, we can use
define-syntax-rules
, which is the preferred way in Racket.
1 (define-syntax-rule (swap a b)
2 (let ((tmp a))
3 (set! a b)
4 (set! b tmp)))
This syntax will create a hygienic macro that does not have the issues that define-macro has.
For another example, let’s create a macro that will allow us to run some command infinitely:
1 (define-syntax-rule (loop f)
2 (letrec ([fix (lambda (k) (k) (fix k))])
3 (fix (lambda () f))))
Note that, if loop was a procedure instead, then (loop (displayln "test")) would not have worked since it would have evaluated the call to displayln right away. We had to pass (lambda () (displayln "test")) as a trick to defer evaluation, and then use fix to keep evaluating this lambda over and over.
The
loop macro
allows us to do
(loop (print (eval (read) ns)))—note the REPL acronym. The expression
eval <...> ns will try to evaluate the expression
<...> in the
ns namespace/context. To get the base (initial) namespace, we can use the
make-base-namespace procedure. In this case, the namespace will be initially empty, but it may be extended with variables or procedures, depending on which commands are executed.
1 > (define ns (make-base-namespace))
2 > (loop (print (eval (read) ns)))
3 (+ 1 1)
4 2(define x 5)
5 #<void>(+ 2 x)
6 7
The printing is a bit off because it is missing newlines. If we use
displayln in place of
print, it will produce much more readable
output
:
1 > (define ns (make-base-namespace))
2 > (loop (displayln (eval (read) ns)))
3 (+ 1 1)
4 2
5 (define x 5)
6 #<void>
7 (+ 2 x)
8 7