Writing Racket Macros: define-syntax and phases
19 May 2023
There are a bunch of different ways of writing a macro in Racket. There are also some tricky things around phases to keep in mind. This is to help me keep them all straight.
3+1 ways to make a macro #
(define-syntax-rule (foo args ...) (use args ...))
is equivalent to:
(define-syntax foo (syntax-rules () ([foo args ...] (use args ...))))
Which, is in turn equivalent to:
(define-syntax foo (λ (stx) (syntax-case stx () [(gensymed-foo args ...) #'(use args ...)]))) ; gensymed-foo is like foo but doesn't match in the template
syntax-rules expands to
syntax-case with some fancy wrapping around it.
syntax-case the most powerful of them all, and it’s here that we’re treating syntax as data comes to the forefront: you can bind the syntax object directly (in our example,
with the (λ (stx) ...) part), pattern match on it, and finally return new syntax with the
define-syntax-rule is the weakest of the three, but handles a common case: just a single form that you’d like to transform a little bit. This version doesn’t allow for writing multiple clauses.
syntax-rules is in the middle: the bodies of each of the rule match arms (
(use args ...)) are assumed to be syntax objects. This works well for the majority of cases I think. It’s only when you need to do really hairy stuff and manually generate code that can’t be put together with repeats (
...) that you need the full power of
syntax-case at your disposal.
Note that there are two forms of
(define-syntax (id stx) body ...) is shorthand for
(define-syntax id (λ (stx) body ...)) much like the shorthand for building functions with
Bonus: more power #
A cousin of
syntax-parse. I was confused about this one for a bit because the names are so close, and they share a lot of similarities.
syntax-case’s documentation is in the
Racket Reference proper, while
syntax-parse’s documentation lives with the
syntax module documentation.
Our previous example would be written like this:
(define-syntax (foo stx) (syntax-parse stx [(_ args ...) #'(use args ...)]))
(define-syntax foo (λ (stx) (syntax-parse stx [(_ args ...) #'(use args ...)])))
syntax-parse supports keyword arguments like
#:when to do some pattern matching and predicate checking.
syntax-parse will backtrack through match arms until it finds a matching and satisfying clause.
As far as I can tell,
syntax-parse is strictly the most powerful of the syntax manipulating forms that I’ve outlined here.
It doesn’t seem like macros can use the functions in their current module by default. However, if you wrap your function definitions in
begin-for-syntax, this shifts the function definitions “up” a phase, and they can be used at the same level as functions.
(begin-for-syntax (define foo (stx) (add-stx-prop stx 'bar 'baz))) (define-syntax my-macro (syntax-parse stx #:with foo-ed (foo stx) #'foo-ed))
You can also
require a module with the
(require (for-syntax "util.rkt"))
For more information on phases, see the Racket Docs on phase levels.