Just to compare: here's what your macro looks like if you push most of the work off to the compile-time phase:
#lang racket
(require (for-syntax racket/format
racket/syntax))
(struct Person (name location))
(define-syntax (what stx)
(syntax-case stx (is)
((what is x thing)
#'(what is x's thing))
((what is x quote-s thing)
(let* ((thing-str (~a (syntax-e #'thing)))
(thing-proper-str (cadr (regexp-match #rx"(.*)\\?" thing-str)))
(thing-id (format-id stx "Person-~a" thing-proper-str)))
#`(#,thing-id x)))))
(define John (Person "John Luser" "Here"))
(what is John's name?)
One thing to note is that we're doing (require (for-syntax ...))
because we're using those libraries at compile-time, during the pre-processing of the program's source code.
Also, note that it's a little simpler because since we're not really doing any munging of the x
being acted on, we can just keep that piece of syntax preserved in the outputted syntax.
If it helps, think about your experience in other languages. If you're familiar with Java, consider what happens if you write the expression: "hello " + "world"
somewhere in your program, and then compile the program. Do you expect the Java compiler to produce bytecode that, when executed, constructs a "hello "
string, a "world"
string, and the string concatenation between the two? Most would say "No, the compiler should rewrite that to just the "hello world"
literal as a part of compilation".
This is preprocessing. When you write macros in Racket, you are teaching Racket more of these preprocessing rules.
Here, let us try the following: we'll add a cat
command that knows how to concatenate strings. That is, let's bring that concatenation example to life.
Here's version one:
#lang racket
(define (cat x y)
(string-append x y))
(define msg-head "hello ")
(define msg-tail " world")
(cat msg-head msg-tail)
(cat "hiya " "world")
Now, we know that as a regular function, both uses of cat
here reduce eventually down to calls to the string-append
function. The compiler isn't smart enough to know how to touch the second use of cat
.
But what if we wanted to do the same sort of compiler rewrite rule: if the compiler sees up front that the source code is trying to cat
two string literals, why not do that at compile-time, so that the emitted bytecode is better?
Well, let's do that. Version two follows:
#lang racket
(define-syntax (cat stx)
(syntax-case stx ()
[(_ x y)
;; At this point, the following code is being run by the _compiler_.
(cond
;; If in the source code we're transforming, both x and y are
;; strings, we can do the concatenation at _compile time_.
[(and (string? (syntax-e #'x))
(string? (syntax-e #'y)))
(let ([xy
(string-append (syntax-e #'x) (syntax-e #'y))])
;; Once we have this string, we still need to emit it back as the
;; result of the rewrite. We want to produce a piece of _syntax_
;; in place of the original stx.
(datum->syntax stx xy))]
[else
;; Otherwise, we want to produce a piece of syntax that looks like a
;; plain function call to string-append.
(datum->syntax stx (list 'string-append #'x #'y))])]))
(define msg-head "hello ")
(define msg-tail " world")
(cat msg-head msg-tail)
(cat "hiya " "world")
Now the problem with this kind of transformations is one of observation: if we did this right, no one should be able to tell the difference! :P Otherwise, it would be a broken transformation. So to see the difference more easily, press the Macro Stepper button on the DrRacket toolbar: it invokes the compiler, but shows you what transformations happen to your program right before it gets turned into bytecode.