Fear of Macros

Contents:
4.1 "A pattern variable can’t be used outside of a template" |
1 Preface
I learned Racket after 25 years of mostly using C and C++.
Some psychic whiplash resulted.
"All the parentheses" was actually not a big deal. Instead, the first +
Fear of Macros

Contents:
1 Preface
I learned Racket after 25 years of mostly using C and C++.
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 @@ -22,12 +22,12 @@ problems or annoyances. I learn more quickly and deeply when I discover the answer to a question I already have, or find the solution to a problem whose pain I already feel. Therefore I’ll give you the questions and problems first, so that you can better appreciate and -understand the answers and solutions.
2 The plan of attack
The macro system you will mostly want to use for production-quality +understand the answers and solutions.
2 Our plan of attack
The macro system you will mostly want to use for production-quality macros is called syntax-parse. And don’t worry, we’ll get to that soon.
But if we start there, you’re likely to feel overwhelmed by concepts and terminology, and get very confused. I did.
1. Instead let’s start with the basics: A syntax object and a function -to change it (a "transformer"). We’ll work at that level for awhile to -get comfortable and to de-mythologize this whole macro business.
2. Next, we’ll realize that some pattern-matching would make life
+to change it—
2. Soon we’ll realize that pattern-matching would make life easier. We’ll learn about syntax-case and its shorthand cousin, define-syntax-rule. We’ll discover we can get confused if we want to munge pattern variables before sticking them @@ -42,7 +42,7 @@ they’re used in error. Normal Racket functions optionally can have contracts and types. These catch usage mistakes and provide clear, useful error messages. It would be great if there were something similar for macro. 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 トランスフォーマ +enhancements is syntax-parse.
3 Transform!
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 simply a function. The function takes syntax and returns syntax. It transforms syntax.
Here’s a transformer function that ignores its input syntax, and always outputs syntax for a string literal:
> (define-syntax foo (lambda (stx) (syntax "I am foo")))
Using it:
> (foo) "I am foo"
When we use define-syntax, we’re making a transformer @@ -59,24 +59,24 @@ which is #’(displayln "hi")?
> (define-syntax (say-hi stx) #'(displayln "hi")) > (say-hi) hi
When Racket expands our program, it sees the occurrence of (say-hi), and sees it has a transformer function for that. It calls our function with the old syntax, and we return the new syntax, -which is used to evaluate and run our program.
3.2 What’s the input?
Our examples so far ignored the input syntax, and output a fixed -syntax. But instead of throwing away the input, usually we want to -transform the input.
Let’s start by looking closely at what the input actually is:
> (define-syntax (show-me stx) (print stx) #'(void)) > (show-me '(i am a list)) #<syntax:10:0 (show-me (quote (i am a list)))>
The (print stx) shows what our transformer is given: a syntax +which is used to evaluate and run our program.
3.2 What’s the input?
Our examples so far have ignored the input syntax and output some +fixed syntax. But typically we will want to transform in the input +syntax into somehing else.
Let’s start by looking closely at what the input actually is:
> (define-syntax (show-me stx) (print stx) #'(void)) > (show-me '(i am a list)) #<syntax:10:0 (show-me (quote (i am a list)))>
The (print stx) shows what our transformer is given: a syntax object.
A syntax object consists of several things. The first part is the -s-expression representing the code, such as '(+ 1 2).
Racket syntax is also decorated with some interesting information such +S-expression representing the code, such as '(+ 1 2).
Racket syntax is also decorated with some interesting information such as the source file, line number, and column. Finally, it has information about lexical scoping (which you don’t need to worry about now, but will turn out to be important later.)
There are a variety of functions available to access a syntax object. Let’s define a piece of syntax:
> (define stx #'(if x (list "true") #f)) > stx #<syntax:11:0 (if x (list "true") #f)>
Now let’s use functions that access the syntax object. The source -information functions:
(syntax-source stx) is returning 'eval, +information functions are:
(syntax-source stx) is returning 'eval, only becaue of how I’m generating this documentation, using an evaluator to run code snippets in Scribble. Normally this would be somthing like "my-file.rkt".
> (syntax-source stx) 'eval
> (syntax-line stx) 11
> (syntax-column stx) 0
More interesting is the syntax "stuff" itself. syntax->datum -converts it completely into an s-expression:
> (syntax->datum stx) '(if x (list "true") #f)
Whereas syntax-e only goes "one level down". It may return a +converts it completely into an S-expression:
> (syntax->datum stx) '(if x (list "true") #f)
Whereas syntax-e only goes "one level down". It may return a list that has syntax objects:
> (syntax-e stx) '(#<syntax:11:0 if> #<syntax:11:0 x> #<syntax:11:0 (list "true")> #<syntax:11:0 #f>)
Each of those syntax objects could be converted by syntax-e, and so on recursively—
which is what syntax->datum does. In most cases, syntax->list gives the same result as -syntax-e:
> (syntax->list stx) '(#<syntax:11:0 if> #<syntax:11:0 x> #<syntax:11:0 (list "true")> #<syntax:11:0 #f>)
When would syntax-e and syntax->list differ? Let’s -not get side-tracked now.
When we want to transform syntax, we’ll generally take the pieces we +syntax-e:
> (syntax->list stx) '(#<syntax:11:0 if> #<syntax:11:0 x> #<syntax:11:0 (list "true")> #<syntax:11:0 #f>)
(When would syntax-e and syntax->list differ? Let’s +not get side-tracked now.)
When we want to transform syntax, we’ll generally take the pieces we 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"
Understand Yoda, can we. Great, but how does this work?
First we take the input syntax, and give it to @@ -86,7 +86,7 @@ list:
("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 -compiler, and that syntax is evaluated:
> (values "i" "am" "backwards") "i"
"am"
"backwards"
3.4 Compile time vs. run time
(define-syntax (foo stx) (make-pipe) ;This is not run time. #'(void)) Normal Racket code runs at ... run time. Duh.
Instead of "compile time vs. run time", you may hear it +compiler, and that syntax is evaluated:
> (values "i" "am" "backwards") "i"
"am"
"backwards"
3.4 Compile time vs. run time
(define-syntax (foo stx) (make-pipe) ;Ce n'est pas le temps d'exécution #'(void)) Normal Racket code runs at ... run time. Duh.
Instead of "compile time vs. run time", you may hear it described as "syntax phase vs. runtime phase". Same difference.
But a syntax transformer is called by Racket as part of the process of parsing, expanding, and compiling our program. In other words, our syntax transformer function is evaluated at compile time.
This aspect of macros lets you do things that simply aren’t possible @@ -123,7 +123,7 @@ something like racket/match, we have to require it ourselves—
and require it for compile time. Instead of using plain (require racket/match), the way to say this is to use (require (for-syntax racket/match))— the for-syntax -part meaning, "for compile time". So let’s try that:
> (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]))])) > (our-if-using-match-v2 #t "true" "false") "true"
3.5 begin-for-syntax
We used for-syntax to require the +part meaning, "for compile time".
So let’s try that:
> (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]))])) > (our-if-using-match-v2 #t "true" "false") "true"
Joy.
3.5 begin-for-syntax
We used for-syntax to require the racket/match module because we needed to use match at compile time.
What if we wanted to define our own helper function to be used by a macro? One way to do that is put it in another module, and @@ -137,7 +137,7 @@ news is this means we can do things like rearrange the pieces of syntax without evaluating them. We can implement forms like if that simply couldn’t work properly as run time functions.
More good news is that there isn’t some special, weird language for writing syntax transformers. We can write these transformer -functions using the Racket language we already know and lovex.
The semi-bad news is that the familiarity can make it easy to forget +functions using the Racket language we already know and love.
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 automatically. If we need other modules, we have to require them, and @@ -173,7 +173,7 @@ shorthand.
Most of the materials I found for learning macros, including th Racket Guide, do a very good job explaining how patterns and templates work. I’m not going to regurgitate that here.
Sometimes, we need to go a step beyond the pattern and template. Let’s look at some examples, how we can get confused, and how to get it -working.
4.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, +working.
4.1 Pattern variable vs. template—
fight! 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 macro does something like this: (struct foo (field1 field2)) automatically defines a number of functions whose names are variations @@ -193,13 +193,13 @@ size" template. Let’s try that:
Even if you prefer mostly to use Emacs, this is a situation where it’s definitely worth temporarily using DrRacket for its Macro Stepper.
The Macro Stepper says that the use of our macro:
(hyphen-define/wrong1.1 foo bar () #t)
expanded to:
(define (name) #t)
Well that explains it. Instead, we wanted to expand to:
(define (foo-bar) #t)
Our template is using the symbol name but we wanted its -value, such as foo-bar in this use of our macro.
Can we think of something we already know that behaves like -this—
where using a variable in the template yields its value? Sure -we do: Pattern variables. Our pattern doesn’t include name -because we don’t expect it in the original syntax— indeed the whole -point of this macro is to create it. So name can’t be in the -main pattern. Fine— let’s make an additional pattern. We can -do that using an additional, nested syntax-case:
> (define-syntax (hyphen-define/wrong1.2 stx) (syntax-case stx () [(_ a b (args ...) body0 body ...) (syntax-case (datum->syntax stx (string->symbol (format "~a-~a" #'a #'b))) () [name #'(define (name args ...) body0 body ...)])])) Looks weird? Let’s take a deep breath. Normally our transformer +value, such as foo-bar in this use of our macro.
Is there anything we already know that behaves like this—
where using +a variable in the template yields its value? Yes: Pattern +variables. Our pattern doesn’t include name because we don’t +expect it in the original syntax— indeed the whole point of this +macro is to create it. So name can’t be in the main +pattern. Fine— let’s make an additional pattern. We can do +that using an additional, nested syntax-case:
> (define-syntax (hyphen-define/wrong1.2 stx) (syntax-case stx () [(_ a b (args ...) body0 body ...) (syntax-case (datum->syntax stx (string->symbol (format "~a-~a" #'a #'b))) () [name #'(define (name args ...) body0 body ...)])])) Looks weird? Let’s take a deep breath. Normally our transformer function is given syntax by Racket, and we pass that syntax to syntax-case. But we can also create some syntax of our own, on the fly, and pass that to syntax-case. That’s all @@ -209,9 +209,9 @@ syntax that we’re creating on the fly. We can give that to 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:
> (hyphen-define/wrong1.2 foo bar () #t) > (foo-bar) foo-bar: undefined;
cannot reference an identifier before its definition
in module: 'program
Hmm. foo-bar is still not defined. Back to the Macro -Stepper. It says now we’re expanding to:
(define (|#<syntax:11:24foo>-#<syntax:11:28 bar>|) #t)
Oh right: #'a and #'b are syntax objects. Therefore
(string->symbol (format "~a-~a" #'a #'b))
is something like
|#<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 -foo and bar). Let’s use syntax->datum to -get it:
> (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) #t
And now it works!
Now for two shortcuts.
Instead of an additional, nested syntax-case we could use +Stepper. It says now we’re expanding to:
(define (|#<syntax:11:24foo>-#<syntax:11:28 bar>|) #t)
Oh right: #'a and #'b are syntax objects. Therefore
(string->symbol (format "~a-~a" #'a #'b))
is the printed form of both syntax objects, joined by a hyphen:
|#<syntax:11:24foo>-#<syntax:11:28 bar>|
Instead we want the datum in the syntax objects, such as the symbols +foo and bar. Which we get using +syntax->datum:
> (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) #t
And now it works!
Now for two shortcuts.
Instead of an additional, nested syntax-case, we could use with-syntaxAnother name for with-syntax could be, "define pattern variable".. This rearranges the syntax-case to look more like a let @@ -227,7 +227,7 @@ 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:
> (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) 100
To review:
You can’t use a pattern variable outside of a template. But you can use syntax or #’ on a pattern variable to make -an ad hoc "fun size" template.
If you want to munge pattern variables for use in the +an ad hoc, "fun size" template.
If you want to munge pattern variables for use in the template, with-syntax is your friend, because it lets you create new pattern variables.
Usually you’ll need to use syntax->datum to get the interesting value inside.
format-id is convenient for formatting identifier @@ -244,16 +244,19 @@ will be named struct-field (the name of the struct, a hyphen, and the field name).
> (require (for-syntax racket/syntax))
> (define-syntax (our-struct stx) (syntax-case stx () [(_ id (fields ...)) (with-syntax ([pred-id (format-id stx "~a?" #'id)]) #`(begin ; Define a constructor. (define (id fields ...) (apply vector (cons 'id (list fields ...)))) ; Define a predicate. (define (pred-id v) (and (vector? v) (eq? (vector-ref v 0) 'id))) ; Define an accessor for each field. #,@(for/list ([x (syntax->list #'(fields ...))] [n (in-naturals 1)]) (with-syntax ([acc-id (format-id stx "~a-~a" #'id x)] [ix n]) #`(define (acc-id v) (unless (pred-id v) (error 'acc-id "~a is not a ~a struct" v 'id)) (vector-ref v ix))))))])) ; Test it out > (require rackunit) > (our-struct foo (a b)) > (define s (foo 1 2)) > (check-true (foo? s)) > (check-false (foo? 1)) > (check-equal? (foo-a s) 1) > (check-equal? (foo-b s) 2)
> (check-exn exn:fail? (lambda () (foo-a "furble"))) ; The tests passed. ; Next, what if someone tries to declare: > (our-struct "blah" ("blah" "blah")) format-id: contract violation
expected: (or/c string? symbol? identifier? keyword? char?
number?)
given: #<syntax:78:0 "blah">
The error message is not very helpful. It’s coming from format-id, which is a private implementation detail of our macro.
You may know that a syntax-case clause can take an optional "guard" or "fender" expression. Instead of
[pattern template]
It can be:
[pattern guard template]
Let’s add a guard expression to our clause:
> (require (for-syntax racket/syntax))
> (define-syntax (our-struct stx) (syntax-case stx () [(_ id (fields ...)) ; Guard or "fender" expression: (for-each (lambda (x) (unless (identifier? x) (raise-syntax-error #f "not an identifier" stx x))) (cons #'id (syntax->list #'(fields ...)))) (with-syntax ([pred-id (format-id stx "~a?" #'id)]) #`(begin ; Define a constructor. (define (id fields ...) (apply vector (cons 'id (list fields ...)))) ; Define a predicate. (define (pred-id v) (and (vector? v) (eq? (vector-ref v 0) 'id))) ; Define an accessor for each field. #,@(for/list ([x (syntax->list #'(fields ...))] [n (in-naturals 1)]) (with-syntax ([acc-id (format-id stx "~a-~a" #'id x)] [ix n]) #`(define (acc-id v) (unless (pred-id v) (error 'acc-id "~a is not a ~a struct" v 'id)) (vector-ref v ix))))))])) ; Now the same misuse gives a better error message: > (our-struct "blah" ("blah" "blah")) eval:81:0: our-struct: not an identifier
at: "blah"
in: (our-struct "blah" ("blah" "blah"))
Later, we’ll see how syntax-parse makes it even easier to -check usage and provide helpful messages about mistakes.
4.3 Using dot notation for nested hash lookups
The previous two examples used a macro to define functions whose names +check usage and provide helpful messages about mistakes.
4.3 Using dot notation for nested hash lookups
The previous two examples used a macro to define functions whose names were made by joining identifiers provided to the macro. This example does the opposite: The identifier given to the macro is split into pieces.
If you write programs for web services you deal with JSON, which is represented in Racket by a jsexpr?. JSON often has dictionaries that contain other dictionaries. In a jsexpr? -these are represented by nested hasheq tables.
JavaScript you can use dot notation:
foo = js.a.b.c; In Racket it’s not so convenient:
; Nested hasheqs typical of a jsexpr: > (define js (hasheq 'a (hasheq 'b (hasheq 'c "value")))) ; Typical annoying code to get something: > (hash-ref (hash-ref (hash-ref js 'a) 'b) 'c) "value"
We can write a helper function to make this a bit cleaner:
; This helper function:
> (define/contract (hash-refs h ks [def #f]) ((hash? (listof any/c)) (any/c) . ->* . any) (with-handlers ([exn:fail? (const (cond [(procedure? def) (def)] [else def]))]) (for/fold ([h h]) ([k (in-list ks)]) (hash-ref h k)))) ; Lets us say: > (hash-refs js '(a b c)) "value"
That’s not bad. Can we go even further and use a dot notation somewhat -like JavaScript?
; This macro: > (require (for-syntax racket/syntax))
> (define-syntax (hash.refs stx) (syntax-case stx () [(_) (raise-syntax-error #f "Expected (hash.key0[.key1 ...] [default])" stx #'chain)] [(_ chain) #'(hash.refs chain #f)] [(_ chain default) (unless (symbol? (syntax-e #'chain)) (raise-syntax-error #f "Expected (hash.key0[.key1 ...] [default])" stx #'chain)) (let ([xs (map (lambda (x) (datum->syntax stx (string->symbol x))) (regexp-split #rx"\\." (symbol->string (syntax->datum #'chain))))]) (unless (and (>= (length xs) 2) (not (eq? (syntax-e (cadr xs)) '||))) (raise-syntax-error #f "Expected hash.key" stx #'chain)) (with-syntax ([h (car xs)] [ks (cdr xs)]) #'(hash-refs h 'ks default)))])) ; Gives us "sugar" to say this: > (hash.refs js.a.b.c) "value"
It works!
We’ve started to appreciate that our macros should give helpful -messages when used in error. We tried to do that here. Let’s -deliberately elicit various errors. Are the messages helpful?
> (hash.refs) eval:87:0: hash.refs: Expected (hash.key0[.key1 ...]
[default])
at: chain
in: (hash.refs)
> (hash.refs 0) eval:90:0: hash.refs: Expected (hash.key0[.key1 ...]
[default])
at: 0
in: (hash.refs 0 #f)
> (hash.refs js) eval:91:0: hash.refs: Expected hash.key
at: js
in: (hash.refs js #f)
> (hash.refs js.) eval:92:0: hash.refs: Expected hash.key
at: js.
in: (hash.refs js. #f)
Not too bad.
Maybe we’re not convinced that writing (hash.refs js.a.b.c) +these are represented by nested hasheq tables:
; Nested `hasheq's typical of a jsexpr: > (define js (hasheq 'a (hasheq 'b (hasheq 'c "value")))) In JavaScript you can use dot notation:
foo = js.a.b.c; In Racket it’s not so convenient:
We can write a helper function to make this a bit cleaner:
; This helper function:
> (define/contract (hash-refs h ks [def #f]) ((hash? (listof any/c)) (any/c) . ->* . any) (with-handlers ([exn:fail? (const (cond [(procedure? def) (def)] [else def]))]) (for/fold ([h h]) ([k (in-list ks)]) (hash-ref h k)))) ; Lets us say: > (hash-refs js '(a b c)) "value"
That’s better. Can we go even further and use a dot notation somewhat +like JavaScript?
; This macro: > (require (for-syntax racket/syntax))
> (define-syntax (hash.refs stx) (syntax-case stx () ; If the optional `default' is missing, assume it's #f. [(_ chain) #'(hash.refs chain #f)] [(_ chain default) (let ([xs (map (lambda (x) (datum->syntax stx (string->symbol x))) (regexp-split #rx"\\." (symbol->string (syntax->datum #'chain))))]) (with-syntax ([h (car xs)] [ks (cdr xs)]) #'(hash-refs h 'ks default)))])) ; Gives us "sugar" to say this: > (hash.refs js.a.b.c) "value"
; Try finding a key that doesn't exist: > (hash.refs js.blah) #f
; Try finding a key that doesn't exist, specifying the default: > (hash.refs js.blah 'did-not-exist) 'did-not-exist
It works!
We’ve started to appreciate that our macros should give helpful +messages when used in error. Let’s try to do that here.
> (require (for-syntax racket/syntax))
> (define-syntax (hash.refs stx) (syntax-case stx () ; Check for no args at all [(_) (raise-syntax-error #f "Expected (hash.key0[.key1 ...] [default])" stx #'chain)] [(_ chain) #'(hash.refs chain #f)] [(_ chain default) ; Check that chain is a symbol, not e.g. a number or string (unless (symbol? (syntax-e #'chain)) (raise-syntax-error #f "Expected (hash.key0[.key1 ...] [default])" stx #'chain)) (let ([xs (map (lambda (x) (datum->syntax stx (string->symbol x))) (regexp-split #rx"\\." (symbol->string (syntax->datum #'chain))))]) ; Check that we have at least hash.key (unless (and (>= (length xs) 2) (not (eq? (syntax-e (cadr xs)) '||))) (raise-syntax-error #f "Expected hash.key" stx #'chain)) (with-syntax ([h (car xs)] [ks (cdr xs)]) #'(hash-refs h 'ks default)))])) ; See if we catch each of the misuses > (hash.refs) eval:91:0: hash.refs: Expected (hash.key0[.key1 ...]
[default])
at: chain
in: (hash.refs)
> (hash.refs 0) eval:93:0: hash.refs: Expected (hash.key0[.key1 ...]
[default])
at: 0
in: (hash.refs 0 #f)
> (hash.refs js) eval:94:0: hash.refs: Expected hash.key
at: js
in: (hash.refs js #f)
> (hash.refs js.) eval:95:0: hash.refs: Expected hash.key
at: js.
in: (hash.refs js. #f)
Not too bad. Of course, the version with error-checking is quite a bit +longer. Error-checking code generally tends to obscure the logic, and +does here. Fortuantely we’ll soon see how syntax-parse can +help mitigate that, in much the same way as contracts in normal +Racket or types in Typed Racket.
Maybe we’re not convinced that writing (hash.refs js.a.b.c) is really clearer than (hash-refs js '(a b c)). Maybe we won’t actually use this approach. But the Racket macro system makes it a possible choice.
5 Syntax parameters
"Anaphoric if" or "aif" is a popular macro example. Instead of writing:
(let ([tmp (big-long-calculation)]) (if tmp (foo tmp) #f)) You could write:
(aif (big-long-calculation) (foo it) #f) In other words, when the condition is true, an it identifier @@ -266,7 +269,7 @@ the code surrounding our macro.
The Racket
If a normal function defines a variable named x, it won’t -conflict with a variable named x in an outer scope:
> (let ([x "outer"]) (let ([x "inner"]) (printf "The inner `x' is ~s\n" x)) (printf "The outer `x' is ~s\n" x))
The inner `x' is "inner"
The outer `x' is "outer"
When your macros also respect lexical scoping, it’s easy to write +conflict with a variable named x in an outer scope:
> (let ([x "outer"]) (let ([x "inner"]) (printf "The inner `x' is ~s\n" x)) (printf "The outer `x' is ~s\n" x))
The inner `x' is "inner"
The outer `x' is "outer"
When our macros also respect lexical scoping, it’s easier to write reliable macros that behave predictably.
So that’s wonderful default behavior. But sometimes we want to introduce a magic variable on purpose—
such as it for aif. The way to do this is with a "syntax parameter", using @@ -278,7 +281,7 @@ default. Only inside of our aif will it have a meani value:
> (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)) Inside the syntax-parameterize, it acts as an alias for tmp. The alias behavior is created by make-rename-transformer.
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) it: can only be used inside aif
But we can still define it as a normal variable:
> (define it 10) > it 10
6 Robust macros: syntax-parse
TO-DO.
TO-DO.
TO-DO.
7 What’s the point of splicing-let?
I stared at racket/splicing for the longest time. What does +it isn’t otherwise defined, we get an error like we want:
> (displayln it) it: can only be used inside aif
But we can still define it as a normal variable:
> (define it 10) > it 10
6 What’s the point of splicing-let?
I stared at racket/splicing for the longest time. What does it do? Why would I use it? Why is it in the Macros section of the reference?
Step one, cut a hole in the box de-mythologize it. For example, using splicing-let like this:
> (require racket/splicing)
> (splicing-let ([x 0]) (define (get-x) x)) ; get-x is visible out here: > (get-x) 0
; but x is not: > x x: undefined;
cannot reference an identifier before its definition
in module: 'program
is equivalent to:
> (define get-y (let ([y 0]) (lambda () y))) ; get-y is visible out here: > (get-y) 0
; but y is not: > y y: undefined;
cannot reference an identifier before its definition
in module: 'program
This is the classic Lisp/Scheme/Racket idiom sometimes called "let @@ -286,8 +289,35 @@ over lambda". koan about closures and objects. A closure hides y, which can only be accessed via get-y.
So why would we care about the splicing forms? They can be more -concise, especially when there are multiple body forms:
> (require racket/splicing)
> (splicing-let ([x 0]) (define (inc) (set! x (+ x 1))) (define (dec) (set! x (- x 1))) (define (get) x)) The splicing variation is more convenient than the usual way:
> (define-values (inc dec get) (let ([x 0]) (values (lambda () ; inc (set! x (+ 1 x))) (lambda () ; dec (set! x (- 1 x))) (lambda () ; get x)))) When there are many body forms—
and you are generating them in a -macro— the splicing variations can be much easier. 8 References and Acknowledgments
Eli Barzliay’s blog post, +concise, especially when there are multiple body forms:
> (require racket/splicing)
> (splicing-let ([x 0]) (define (inc) (set! x (+ x 1))) (define (dec) (set! x (- x 1))) (define (get) x)) The splicing variation is more convenient than the usual way:
> (define-values (inc dec get) (let ([x 0]) (values (lambda () ; inc (set! x (+ 1 x))) (lambda () ; dec (set! x (- 1 x))) (lambda () ; get x)))) When there are many body forms—
and we’re generating them in a +macro— the splicing variations can be much easier. 7 Robust macros: syntax-parse
Functions can be used in error. So can macros.
7.1 Error-handling strategies for functions
With plain old functions, we have several choices.
1. Don’t check at all.
> (define (misuse s) (string-append s " snazzy suffix")) ; User of the function: > (misuse 0) string-append: contract violation
expected: string?
given: 0
argument position: 1st
other arguments...:
" snazzy suffix"
; I guess I goofed, but – what is this "string-append" of which you ; speak?? The problem is that the resulting error message will be confusing. Our +user thinks they’re calling misuse, but is getting an error +message from string-append. In this simple example they +could probably guess what’s happening, but in most cases they won’t.
2. Write some error handling code.
> (define (misuse s) (unless (string? s) (error 'misuse "expected a string, but got ~a" s)) (string-append s " snazzy suffix")) ; User of the function: > (misuse 0) misuse: expected a string, but got 0
; I goofed, and understand why! It's a shame the writer of the ; function had to work so hard to tell me. Unfortunately the error code tends to overwhelm and/or obscure our +function definition. Also, the error message is good but not +great. Improving it would require even more error code.
3. Use a contract.
> (define/contract (misuse s) (string? . -> . string?) (string-append s " snazzy suffix")) ; User of the function: > (misuse 0) misuse: contract violation
expected: string?, given: 0
in: the 1st argument of
(-> string? string?)
contract from: (function misuse)
blaming: program
at: eval:125.0
; I goofed, and understand why! I hear the writer of the function is ; happier. This is the best of both worlds.
The contract is a simple and concise. Even better, it’s +declarative. We say what we want, without needing to spell out what to +do.
On the other hand the user of our function gets a very detailed error +message. Plus, the message is in a standard, familiar format.
4. Use Typed Racket.
#lang typed/racket
> (: misuse (String -> String))
> (define (misuse s) (string-append s " snazzy suffix")) > (misuse 0) eval:3:0: Type Checker: Expected String, but got Zero
in: (quote 0)
With respect to error handling, Typed Racket has the same benefits as +contracts. Good.
7.2 Error-handling strategies for macros
For macros, we have similar choices.
1. Ignore the possibility of misuse. This choice is even worse for +macros. The default error messages are even less likely to make sense, +much less help our user know what to do.
2. Write error-handling code. We saw how much this complicated our +macros in our example of Using dot notation for nested hash lookups. And while we’re still +learning how to write macros, we especially don’t want more cognitive +load and obfuscation.
3. Use syntax/parse. For macros, this is the equivalent of +using contracts or types for functions. We can declare that input +pattern elements must be certain kinds of things, such as an +identifier. Instead of "types", the things are referred to as "syntax +classes". There are predefined syntax classes, plus we can define our own.
7.3 Using syntax/parse
November 1, 2012: So here’s the deal. After writing everything up to +this point, I sat down to re-read the documentation for +syntax/parse. It was...very understandable. I didn’t feel +confused.
<span style='accent: "Kenau-Reeves"'> Whoa. </span> Why? The documentation is written very well. Also, everything up to +this point prepared me to appreciate what syntax/parse does, +and why. That leaves the "how" of using it, which seems pretty +straightforward, so far.
This might well be a temporary state of me "not knowing what I don’t +know". As I dig in and use it more, maybe I’ll discover something +confusing or tricky. If/when I do, I’ll come back here and update +this.
But for now I’ll focus on improving the previous parts.
8 References and Acknowledgments
Eli Barzliay’s blog post, Writing ‘syntax-case’ Macros, helped me understand many key details and concepts. It also inspired me to use a "bottom-up" approach. However