Defmacro

Defmacro

Robbert Haarman

2010-12-11


Introduction

defmacro macros are not actually part of Scheme (they are from Common Lisp). However, many Scheme implementations support them, and they are conceptually simple. I will explain defmacro macros first, before moving on to Scheme's syntax-rules macros.


Examples

A defmacro macro is defined as follows:

(defmacro name (arguments) 
	body)

This defines the macro name, which takes the specified arguments. The argument list is just like for lambda. To expand the macro, body is evaluated, and the return value is substituted in place of the original invocation of the macro. A few examples:

; Define a macro that returns the code it is passed
(defmacro id (x) x)
; The following expands to (newline)
(id (newline))

; Define a macro that will cause the argument to be
; evaluated twice
(defmacro twice (x) `(begin ,x ,x))
; Prints four newlines
(twice (twice (newline)))

On a very basic level, that's it. You just do whatever you want with the arguments and return some list. The list will then be processed as if it had been there instead of the original macro invocation. Macros can expand to other macro invocations, and you can make it all as complex as you wish.


pred

Now for a more useful example. Suppose you find yourself writing a lot of code that matches a value against a number of predicates:

(cond
	((number? x) ...)
	((symbol? x) ...)
	((string? x) ...)
	((list? x) ...))

It would be nice if Scheme allowed youto write:

(pred x
	(number? ...)
	(symbol? ...)
	(string? ...)
	(list? ...))

Now, Scheme doesn't have a pred construct. But you can write one! The following macro transforms the pred code above into the cond code before it:

(defmacro pred (expr . alts)
	`(cond
		,@(map
			(lambda (x)
				(cons (list (car x) expr)
					(cdr x)))
			alts)))

Note how the macro processes each alternative by extracting the predicate being matched, and replacing it by a function call to the predicate, with the expression as an argument. Congratulations, you have just extended Scheme with another conditional form! However, there is a problem. Read the following code, think about what you would expect it to do, and how it would be expanded:

(pred (read)
	(symbol? 'symbol)
	(number? 'number)
	(string? 'string))

The intention seems to be to read one expression, and return symbol if it's a symbol, number if it's a number, etc. However, from the expansion, it's clear that this is not the actual behavior:

(cond
	((symbol? (read)) 'symbol)
	((nuber? (read)) 'number)
	((string? (read)) 'string))

read is called up to three times! To bring the behavior in line with expectations, we can use let to first bind the value returned by (read) to a variable, and then pass that variable to pred. Better yet, we can include this in the macro:

(defmacro pred (expr . alts)
	`(let ((-pred-x- ,expr))
		(cond
			,@(map (lambda (x)
				(cons (list (car x) '-pred-x-)
					(cdr x)))
				alts))))

This fixes one problem, but introduces another, much subtler problem, called variable capture. To understand what's happening, consider what would happen if the symbol -pred-x- is actually used in one of the alternatives. For example:

(let ((-pred-x- 12))
	(pred 'foo
		(symbol? (write -pred-x-) (newline))))

You may expect the code above to write 12, but it actually writes foo! This becomes obvious when you look at the expansion:

(let ((-pred-x- 12))
	(let ((-pred-x- 'foo))
		(cond
			((symbol? -pred-x-)
				(write -pred-x-)
				(newline)))))

The inner -pred-x- shadows the outer one, and, as a result, write is passed the value foo. Common Lisp, and probably any Scheme implementation that provides defmacro, includes a function gensym that generates a symbol that is guaranteed to be unique. By using such a symbol for the macro expansion, we can make sure that variable capture does not occur. A typical macro using gensym looks like this:

(defmacro my-macro (...)
	(let ((sym (gensym)))
		`(let ((,sym ...))
			...)))

Note how we first bind the name of our symbol using let, but this let is not actually part of the expansion of the macro. This ensures that variable capture will not occur. However, the readability of the macro certainly has not improved.

There are a few variations on the defmacro syntax used here. The original defmacro system in Common Lisp uses &rest to signal that the next symbol should be bound to the rest of the argument list, rather than Scheme's . used here. TinyScheme uses an even more Scheme-like syntax (analogous to define):

(macro (name arguments) 
...)

You will have to experiment to see how your Scheme implementation supports macros, or use syntax-rules macros, which are in the Scheme standard.

Valid XHTML 1.1! Valid CSS! Viewable with Any Browser