Get to with-syntax by way of a nested syntax-case

Rather than introduce with-syntax as a magic spell, start
by using a nested syntax-case as a more way to define
more pattern variables. Then explain show with-syntax
as simply "sugar" for this.
This commit is contained in:
Greg Hendershott 2012-11-01 16:52:16 -04:00
parent 5b517ae4b9
commit 053c0d53dd

157
main.rkt
View File

@ -723,12 +723,13 @@ No more error---good! Let's try to use it:
(foo-bar) (foo-bar)
] ]
It seems we're defining a function with a name other than Apparently our macro is defining a function with some name other than
@racket[foo-bar]? @racket[foo-bar]. Huh.
This is where the Macro Stepper in DrRacket is invaluable. Even if you This is where the Macro Stepper in DrRacket is
prefer mostly to use Emacs, this is a situation where it's worth using invaluable. @margin-note{Even if you prefer mostly to use Emacs, this
DrRacket at least temporarily for its Macro Stepper. is a situation where it's definitely worth temporarily using DrRacket
for its Macro Stepper.}
@image[#:scale 0.5 "macro-stepper.png"] @image[#:scale 0.5 "macro-stepper.png"]
@ -753,23 +754,39 @@ Well that explains it. Instead, we wanted to expand to:
Our template is using the symbol @racket[name] but we wanted its Our template is using the symbol @racket[name] but we wanted its
value, such as @racket[foo-bar] in this use of our macro. value, such as @racket[foo-bar] in this use of our macro.
A solution here is @racket[with-syntax]@margin-note*{You could Can we think of something we already know that behaves like
consider @racket[with-syntax] to mean, "define pattern variables".}, this---where using a variable in the template yields its value? Sure
which lets us say that @racket[name] is something whose value can be we do: Pattern variables. Our pattern doesn't include @racket[name]
used in our output template. In effect, it lets us say that because we don't expect it in the original syntax---indeed the whole
@racket[name] is an additional pattern variable. point of this macro is to create it. So @racket[name] can't be in the
main pattern. Fine---let's make an @italic{additional} pattern. We can
do that using an additional, nested @racket[syntax-case]:
@i[ @i[
(define-syntax (hyphen-define/wrong1.3 stx) (define-syntax (hyphen-define/wrong1.2 stx)
(syntax-case stx () (syntax-case stx ()
[(_ a b (args ...) body0 body ...) [(_ a b (args ...) body0 body ...)
(with-syntax ([name (datum->syntax stx (syntax-case (datum->syntax stx
(string->symbol (format "~a-~a" (string->symbol (format "~a-~a" #'a #'b))) ()
#'a [name #'(define (name args ...)
#'b)))]) body0 body ...)])]))
#'(define (name args ...) ]
body0 body ...))]))
(hyphen-define/wrong1.3 foo bar () #t) Looks weird? Let's take a deep breath. Normally our transformer
function is given syntax by Racket, and we pass that syntax to
@racket[syntax-case]. But we can also create some syntax of our own,
on the fly, and pass @italic{that} to @racket[syntax-case]. That's all
we're doing here. The whole @racket[(datum->syntax ...)] expression is
syntax that we're creating on the fly. We can give that to
@racket[syntax-case], and match it using a pattern variable named
@racket[name]. Voila, we have a new pattern variable. We can use it in
a template, and its value will go in the template.
We might have one more---just one, I promise!---small problem left.
Let's try to use our new version:
@i[
(hyphen-define/wrong1.2 foo bar () #t)
(foo-bar) (foo-bar)
] ]
@ -780,13 +797,47 @@ Stepper. It says now we're expanding to:
(define (|#<syntax:11:24foo>-#<syntax:11:28 bar>|) #t) (define (|#<syntax:11:24foo>-#<syntax:11:28 bar>|) #t)
] ]
Oh right: @racket[#'a] and @racket[#'b] are syntax objects, and Oh right: @racket[#'a] and @racket[#'b] are syntax objects. Therefore
@racket[format] is printing them as such. Instead we want the datum in
the syntax objects (the symbols @racket[foo] and @racket[bar]). Let's @racketblock[(string->symbol (format "~a-~a" #'a #'b))]
use @racket[syntax->datum]:
is something like
@racketblock[|#<syntax:11:24foo>-#<syntax:11:28 bar>|]
---the printed form of both syntax objects, joined by a hyphen.
Instead we want the datum in the syntax objects (such as the symbols
@racket[foo] and @racket[bar]). Let's use @racket[syntax->datum] to
get it:
@i[ @i[
(define-syntax (hyphen-define/ok1 stx) (define-syntax (hyphen-define/ok1 stx)
(syntax-case stx ()
[(_ a b (args ...) body0 body ...)
(syntax-case (datum->syntax stx
(string->symbol (format "~a-~a"
(syntax->datum #'a)
(syntax->datum #'b)))) ()
[name #'(define (name args ...)
body0 body ...)])]))
(hyphen-define/ok1 foo bar () #t)
(foo-bar)
]
And now it works!
Now for two shortcuts.
Instead of an additional, nested @racket[syntax-case] we could use
@racket[with-syntax]@margin-note*{Another name for
@racket[with-syntax] could be, "define pattern variable".}. This
rearranges the @racket[syntax-case] to look more like a @racket[let]
statement---first the name, then the value. Also it's more convenient
if we need to define more than one pattern variable.
@i[
(define-syntax (hyphen-define/ok2 stx)
(syntax-case stx () (syntax-case stx ()
[(_ a b (args ...) body0 body ...) [(_ a b (args ...) body0 body ...)
(with-syntax ([name (datum->syntax stx (with-syntax ([name (datum->syntax stx
@ -795,45 +846,69 @@ use @racket[syntax->datum]:
(syntax->datum #'b))))]) (syntax->datum #'b))))])
#'(define (name args ...) #'(define (name args ...)
body0 body ...))])) body0 body ...))]))
(hyphen-define/ok1 foo bar () #t) (hyphen-define/ok2 foo bar () #t)
(foo-bar) (foo-bar)
] ]
And now it works! Whether you use an additional @racket[syntax-case] or use
@racket[with-syntax], either way you are simply defining an additional
pattern variable. Don't let the terminology and structure make it seem
mysterious.
By the way, there is a utility function in @racket[racket/syntax] Also, there is a utility function in @racket[racket/syntax] called
called @racket[format-id] that lets us format identifier names more @racket[format-id] that lets us format identifier names more
succinctly. As we've learned, we need to @racket[require] the module succinctly. As we've learned, we need to @racket[require] the module
using @racket[for-syntax], since we need it at compile time: using @racket[for-syntax], since we need it at compile time:
@i[ @i[
(require (for-syntax racket/syntax)) (require (for-syntax racket/syntax))
(define-syntax (hyphen-define/ok2 stx) (define-syntax (hyphen-define/ok3 stx)
(syntax-case stx () (syntax-case stx ()
[(_ a b (args ...) body0 body ...) [(_ a b (args ...) body0 body ...)
(with-syntax ([name (format-id stx "~a-~a" #'a #'b)]) (with-syntax ([name (format-id stx "~a-~a" #'a #'b)])
#'(define (name args ...) #'(define (name args ...)
body0 body ...))])) body0 body ...))]))
(hyphen-define/ok2 bar baz () #t) (hyphen-define/ok3 bar baz () #t)
(bar-baz) (bar-baz)
] ]
Using @racket[format-id] is convenient as it handles the tedium of Using @racket[format-id] is convenient as it handles the tedium of
converting from syntax to datum and back again. converting from syntax to symbol datum to string ... and all the way
back.
Finally, here's a variation that accepts any number of name parts that
are joined with hyphens:
@i[
(require (for-syntax racket/string racket/syntax))
(define-syntax (hyphen-define* stx)
(syntax-case stx ()
[(_ (names ...) (args ...) body0 body ...)
(let* ([names/sym (map syntax-e (syntax->list #'(names ...)))]
[names/str (map symbol->string names/sym)]
[name/str (string-join names/str "-")]
[name/sym (string->symbol name/str)])
(with-syntax ([name (datum->syntax stx name/sym)])
#`(define (name args ...)
body0 body ...)))]))
(hyphen-define* (foo bar baz) (v) (* 2 v))
(foo-bar-baz 50)
]
To review: To review:
@itemize[ @itemize[
@item{You can't use a pattern variable outside of a template. But
you can use @racket[syntax] or @tt{#'} on a pattern variable to make
an ad hoc "fun size" template.}
@item{If you want to munge pattern variables for use in the @item{If you want to munge pattern variables for use in the
template, @racket[with-syntax] is your friend.} template, @racket[with-syntax] is your friend, because it lets you
create new pattern variables.}
@item{You will need to use @racket[syntax] or @tt{#'} on the pattern @item{Usually you'll need to use @racket[syntax->datum] to get the
variables to turn them into "fun size" templates.} interesting value inside.}
@item{Usually you'll also need to use @racket[syntax->datum] to get
the interesting value inside.}
@item{@racket[format-id] is convenient for formatting identifier @item{@racket[format-id] is convenient for formatting identifier
names.} names.}
@ -844,10 +919,12 @@ names.}
@subsection{Making our own @racket[struct]} @subsection{Making our own @racket[struct]}
In this example we'll pretend that Racket doesn't already have a Let's apply what we just learned to a more-realistic example. We'll
@racket[struct] capability. Fortunately, we can define a macro to pretend that Racket doesn't already have a @racket[struct]
provide this feature. To keep things simple, our structure will be capability. Fortunately, we can write a macro to provide our own
immutable (read-only) and it won't support inheritance. system for defining and using structures. To keep things simple, our
structure will be immutable (read-only) and it won't support
inheritance.
Given a structure declaration like: Given a structure declaration like:
@ -855,7 +932,7 @@ Given a structure declaration like:
(our-struct name (field1 field2 ...)) (our-struct name (field1 field2 ...))
] ]
We need to define some procedures. We need to define some procedures:
@itemize[ @itemize[