Friday, April 22, 2005

When to reserve keywords

As I've pointed out before, when a macro uses literal keywords, the macro writer should reserve the keywords by binding them to macros that simply raise syntax errors if the keywords are used outside the context of invocations of the macro.

There are in fact two different ways that keywords get used in macros. The first we might describe as true syntactic sugar: in compound-unit/sig, there are always exactly three clauses, and their tags, import, link, and export, serve only to remind the reader of their purpose. Similarly, each (tag : signature) clause in the import clause only contains the : for readability. In this case, rebinding the keyword identifiers will just cause invocations of the macro to fail with a syntax error, because the use of the keyword at the invocation site doesn't have the same binding as the use of the keyword in the macro definition.

The second way that keywords get used, though, is as tags to distinguish different expression forms. In this case, if the keyword gets rebound, then a subexpression of the macro invocation will actually appear as a different kind of expression, which may very well succeed at matching some other case in the macro. The cond macro serves as an example:
(define-syntax cond
(syntax-rules (else)
[(_ [else a-exp]) a-exp]
[(_ [q0-exp a0-exp] [q1-exp a1-exp] ...)
(if q0-exp a0-exp (cond [q1-exp a1-exp] ...))]))
This macro essentially declares the "question" subexpression of each clause to have a kind of union type: a question is either an expression or the keyword else. If, at the macro invocation site, the identifier else isn't bound the same way as it was in the macro definition, it is still an expression, so it matches the second case of the macro:
(let ([else #f])
(cond
[else 23]))
; => #<void>
It's in this second case that reserving keywords is really important. In the first case, you get an error, but you find out right away and can fix it. In the second, you may not get an error at all, or the error may not appear until much later in the program.

1 comment:

Laurent said...

Hi,

Thanks, that was interesting.

Apparently, now MzScheme produces a syntax-error (albeit a succinct one) on the below example.

So, is there now no need to use provide-keyword for such cases?

Thanks,
Laurent

#lang scheme

(define-syntax cond2
(syntax-rules (else)
[(_ [else a-exp])
a-exp]
[(_ [q0-exp a0-exp] [q1-exp a1-exp] ...)
(if q0-exp a0-exp (cond2 [q1-exp a1-exp] ...))]))

; This one is ok, no problem.
(cond2 [#f 1]
[else 2])

; There is now a syntax error in the program.
(let ([else #f])
(cond2 [#f 3]
[else 4]))

(sorry for the indentation mess...)