Fear of Macros
Copyright (c) 2012 by Greg Hendershott. All rights reserved.
4.1.1 "A pattern variable can’t be used outside of a template" |
1 Introduction
I learned Racket after 25 years of doing C/C++ imperative programming.
Some psychic whiplash resulted.
"All the parentheses" was actually not a big deal. Instead, the first +
Fear of Macros
Copyright (c) 2012 by Greg Hendershott. All rights reserved.
4.1.1 "A pattern variable can’t be used outside of a template" |
1 Introduction
I learned Racket after 25 years of doing C/C++ imperative programming.
Some psychic whiplash resulted.
"All the parentheses" was actually not a big deal. Instead, the first mind warp was functional programming. Before long I wrapped my brain around it, and went on to become comfortable and effective with many other aspects and features of Racket.
But two final frontiers remained: Macros and continuations.
I found that simple macros were easy and understandable, plus there @@ -10,7 +10,7 @@ eventually sink in after enough re-readings. I even found myself using trial and error, rather than having a clear mental model what was going on. Gah.
I’m starting to write this at the point where the shapes are slowly emerging from the fog.
My primary motive is selfish. Explaining something forces me to learn -it more thorougly. Plus I expect that if I write something with +it more thoroughly. Plus I expect that if I write something with mistakes, other people will be eager to point them out and correct me. Is that a social-engineering variation of meta-programming? Next question, please. :)
Finally I do hope it may help other people who have a similar @@ -37,7 +37,7 @@ this. We won’t look at them. We also won’t spend a lot of time talki about "hygiene".]
4. Finally, we’ll realize that our macros could be smarter when they’re used in error. Normal Racket functions can optionally have contracts and types. These can catch mistakes and provide clear, -useful error messages. It would be great if there were somthing +useful error messages. It would be great if there were something similar for macros, and there is. One of the more-recent Racket macro enhancements is syntax-parse.
3 Transformers
YOU ARE INSIDE A ROOM. |
THERE ARE KEYS ON THE GROUND. |
THERE IS A SHINY BRASS LAMP NEARBY. |
|
IF YOU GO THE WRONG WAY, YOU WILL BECOME |
HOPELESSLY LOST AND CONFUSED. |
|
> pick up the keys |
|
YOU HAVE A SYNTAX TRANSFORMER |
3.1 What is a syntax transformer?
A syntax transformer is not one of the トランスフォーマ transformers.
Instead, it is quite simple. It is a function. The function takes @@ -68,8 +68,8 @@ now, but will turn out to be important later.)
There are a variety of func were given, maybe rearrange their order, perhaps change some of the pieces, and often introduce brand-new pieces.
3.3 Actually transforming the input
Let’s write a transformer function that reverses the syntax it was given:
> (define-syntax (reverse-me stx) (datum->syntax stx (reverse (cdr (syntax->datum stx))))) > (reverse-me "backwards" "am" "i" values) "i"
"am"
"backwards"
What’s going on here? First we take the input syntax, and give it to -syntax->datum. This converts the syntax into a plain old list:
> (syntax->datum #'(reverse-me "backwards" "am" "i" values)) '(reverse-me "backwards" "am" "i" values)
Using cdr slics off the first item of the list, -reverse-me, leaving the rmainder: +syntax->datum. This converts the syntax into a plain old list:
> (syntax->datum #'(reverse-me "backwards" "am" "i" values)) '(reverse-me "backwards" "am" "i" values)
Using cdr slices off the first item of the list, +reverse-me, leaving the remainder: ("backwards" "am" "i" values). Passing that to reverse changes it to (values "i" "am" "backwards"):
> (reverse (cdr '("backwards" "am" "i" values))) '(values "i" "am")
Finally we use syntax->datum to convert this back to syntax:
> (datum->syntax #f '(values "i" "am" "backwards")) #<syntax (values "i" "am" "backwards")>
That’s what our transformer function gives back to the Racket @@ -95,7 +95,7 @@ change it into a plain Racket list:
the condition, true-expression, and false-expression—it to mean an error by default. Only inside of our aif will it have a meaningful value:from the list using cadr, caddr, and cadddr and arrange them into a cond form:
`(cond [,(cadr xs) ,(caddr xs)] [else ,(cadddr xs)]) 3. Finally, we change that into syntax using -datum->syntax:
> (datum->syntax stx `(cond [,(cadr xs) ,(caddr xs)] [else ,(cadddr xs)])) #<syntax (cond (#t "true") (else "fals...>
So that works, but using cdddr etc. to destructure a list is +datum->syntax:
> (datum->syntax stx `(cond [,(cadr xs) ,(caddr xs)] [else ,(cadddr xs)])) #<syntax (cond (#t "true") (else "fals...>
So that works, but using cadddr etc. to destructure a list is painful and error-prone. Maybe you know Racket’s match? Using that would let us do pattern-matching.
Notice that we don’t care about the first item in the syntax list. We didn’t take (car xs) in our-if-v2, and we @@ -103,7 +103,7 @@ didn’t use name when we used pattern-matching. syntax transformer won’t care about that, because it is the name of the transformer binding. In other words, a macro usually doesn’t care about its own name.
Instead of:
> (define-syntax (our-if-v2 stx) (define xs (syntax->list stx)) (datum->syntax stx `(cond [,(cadr xs) ,(caddr xs)] [else ,(cadddr xs)]))) We can write:
> (define-syntax (our-if-using-match stx) (match (syntax->list stx) [(list name condition true-expr false-expr) (datum->syntax stx `(cond [,condition ,true-expr] [else ,false-expr]))])) > (our-if-using-match #t "true" "false") match: undefined;
cannot reference an identifier before its definition
in module: 'program
phase: 1
But wait, we can’t. It’s complaining that match isn’t -defined. We havne’t required the racket/match module?
It turns out we haven’t. Remember, this transformer function is +defined. We haven’t required the racket/match module?
It turns out we haven’t. Remember, this transformer function is working at compile time, not run time. And at compile time, only racket/base is required for you automatically. If we want something like racket/match, we have to require it @@ -118,23 +118,23 @@ for writing syntax transformers. We can write these transformer functions using familiar Racket code. The semi-bad news is that the familiarity can make it easy to forget that we’re not working at run time. Sometimes that’s important to remember. For example only -racket/base is required for us automtically. If we need other +racket/base is required for us automatically. If we need other modules, we have to require them, and we have to require them for compile time using (require (for-syntax)).
4 Pattern matching: syntax-case and syntax-rules
Most useful syntax transformers work by taking some input syntax, and rearranging the pieces into something else. As we saw, this is -possible but tedious using list accessors such as cdddr. It’s +possible but tedious using list accessors such as cadddr. It’s more convenient and less error-prone to use pattern-matching.
Historically, syntax-case and syntax-parse pattern matching came first. match was added to Racket later.
It turns out that pattern-matching was one of the first improvements to be added to the Racket macro system. It’s called syntax-case, and has a shorthand for simple situations called -define-syntax-rule.
Recall our previous example:
(require (for-syntax racket/match)) (define-syntax (our-if-using-match-v2 stx) (match (syntax->list stx) [(list _ condition true-expr false-expr) (datum->syntax stx `(cond [,condition ,true-expr] [else ,false-expr]))])) Here’s what it looks like using syntax-case:
> (define-syntax (our-if-using-syntax-case stx) (syntax-case stx () [(_ condition true-expr false-expr) #'(cond [condition true-expr] [else false-expr])])) > (our-if-using-syntax-case #t "true" "false") "true"
Prety similar, huh? The pattern part looks almost exactly the +define-syntax-rule.
Recall our previous example:
(require (for-syntax racket/match)) (define-syntax (our-if-using-match-v2 stx) (match (syntax->list stx) [(list _ condition true-expr false-expr) (datum->syntax stx `(cond [,condition ,true-expr] [else ,false-expr]))])) Here’s what it looks like using syntax-case:
> (define-syntax (our-if-using-syntax-case stx) (syntax-case stx () [(_ condition true-expr false-expr) #'(cond [condition true-expr] [else false-expr])])) > (our-if-using-syntax-case #t "true" "false") "true"
Pretty similar, huh? The pattern part looks almost exactly the same. The "template" part—
where we specify the new syntax— is -simpler. We don’t need to do quasiquoting and unquoting. We don’t need +simpler. We don’t need to do quasi-quoting and unquoting. We don’t need to use datum->syntax. We simply supply a template, which uses variables from the pattern. There is a shorthand for simple pattern-matching cases, which expands into syntax-case. It’s called define-syntax-rule:
> (define-syntax-rule (our-if-using-syntax-rule condition true-expr false-expr) (cond [condition true-expr] [else false-expr])) > (our-if-using-syntax-rule #t "true" "false") "true"
Here’s the thing about define-syntax-rule. Because it’s so -simple, define-syntax-rule is ofen the first thing people are +simple, define-syntax-rule is often the first thing people are taught about macros. But it’s almost deceptively simple. It looks so much like defining a normal run time function—
yet it’s not. It’s working at compile time, not run time. Worse, the moment you want to @@ -148,7 +148,7 @@ shorthand. 4.1 Racket Guide, do a very good job explaining how the patterns work. I’m not going to regurgitate that here.
Instead, let’s look at some ways we’re likely to get tripped up.
4.1.1 "A pattern variable can’t be used outside of a template"
Let’s say we want to define a function with a hyphenated name, a-b, but we supply the a and b parts separately. The Racket struct -form does somethin like this—
if we define a struct named +form does something like this— if we define a struct named foo, it defines a number of functions whose names are variations on the name foo, such as foo-field1, foo-field2, foo?, and so on. So let’s pretend we’re doing something like that. We want to @@ -189,7 +189,7 @@ older macros, you can learn about them then.)
> (require racket/stxparam)
> (define-syntax-parameter it (lambda (stx) (raise-syntax-error (syntax-e stx) "can only be used inside aif")))
> (define-syntax-rule (aif condition true-expr false-expr) (let ([tmp condition]) (if tmp (syntax-parameterize ([it (make-rename-transformer #'tmp)]) true-expr) false-expr))) > (aif 10 (displayln it) (void)) 10
> (aif #f (displayln it) (void))
We can still use it as a normal variable name:
> (define it 10) > it 10
If we try to use it outside of an aif form, and it isn’t otherwise defined, we get an error like we want:
> (displayln it) 10
Perfect.
6 Robust macros: syntax-parse
TO-DO.
7 Other questions
Hopefully I will answer these in the course of the other sections. But -just in case:
7.1 What’s the point of with-syntax?
Done.
7.2 What’s the point of begin-for-syntax?
TO-DO.
7.3 What’s the point of racket/splicing?
TO-DO.
8 References/Acknowledgements
Eli Barzliay wrote a blog post, +just in case:
7.1 What’s the point of with-syntax?
Done.
7.2 What’s the point of begin-for-syntax?
TO-DO.
7.3 What’s the point of racket/splicing?
TO-DO.
8 References/Acknowledgments
Eli Barzliay wrote a blog post, Writing ‘syntax-case’ Macros, which explains many key details. However it’s written especially for people already familiar with "un-hygienic" "defmacro" @@ -200,7 +200,7 @@ concise, clear fashion.
Eli Barzilay wrote another blog post, Dirty Looking Hygiene, which explains syntax-parameterize. I relied heavily on that, mostly just updating it since his post was written -before PLT Scheme was renamed to Racket.
9 Epilog
"Before I had studied Chan (Zen) for thirty years, I saw mountains as +before PLT Scheme was renamed to Racket.
9 Epilogue
"Before I had studied Chan (Zen) for thirty years, I saw mountains as mountains, and rivers as rivers. When I arrived at a more intimate knowledge, I came to the point where I saw that mountains are not mountains, and rivers are not rivers. But now that I have got its very