Fear of Macros

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 +
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 @@ -184,29 +184,30 @@ converting from syntax to datum and back again.
Recap: If you want to mung syntax or #’ on the pattern variables to turn them into fun size templates, and often also use syntax->datum to get the interesting value inside. Finally, format-id is -convenient for formatting identifier names.
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))
It would be great to write:
(aif (big-long-calculation) (foo it) #f)
In other words, when the condition is true, an it identifier +convenient for formatting identifier names.
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 is automatically created and set to the value of the condition. This -should be easy:
> (define-syntax-rule (aif condition true-expr false-expr) (let ([it condition]) (if it true-expr false-expr))) > (aif #t (displayln it) (void)) it: undefined;
cannot reference an identifier before its definition
in module: 'program
Wait, what—
It turns out that all along we have been protected from making a -certain kind of mistake in our macros. The mistake is to introduce a -variable that accidentally conflicts with one in the code that is -using our macro.
The Racket Reference -Section -1.2.3.5 Transformer Bindings. has a good explanation of this, and an -example. (You can stop when you reach the part about set! -transformers.) Basically, the input syntax has "marks" to preserve -lexical scope. This makes your macro behave like a normal function. If -a normal function defines a variable named x, it won’t -conflict with a variable named x in an outer scope.
This makes it easy to write reliable macros that behave -predictably. Unfortunately, once in awhile, we want to introduce a -magic variable like it for aif on purpose.
The way to do this is with define-syntax-parameter and +should be easy:
> (define-syntax-rule (aif condition true-expr false-expr) (let ([it condition]) (if it true-expr false-expr))) > (aif #t (displayln it) (void)) it: undefined;
cannot reference an identifier before its definition
in module: 'program
Wait, what? it is undefined?
It turns out that all along we have been protected from making a +certain kind of mistake in our macros. The mistake is if our new +syntax introduces a variable that accidentally conflicts with one in +the code surrounding our macro.
The Racket Reference section, +Transformer +Bindings, has a good explanation and example. Basically, syntax +has "marks" to preserve lexical scope. This makes your macro behave +like a normal function, for lexical scoping.
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 +reliable macros that behave predictably.
So that’s wonderful default behavior. But sometimes we want
+to introduce a magic variable on purpose—
The way to do this is with a "syntax parameter", using +define-syntax-parameter and syntax-parameterize. You’re probably familiar with regular -parameters in Racket.
> (define current-foo (make-parameter "some default value")) > (current-foo) "some default value"
> (parameterize ([current-foo "I have a new value, for now"]) (current-foo)) "I have a new value, for now"
> (current-foo) "some default value"
Historically, there are other ways to do this. If you -know them, you will want to unlearn them. But if you’re the target -audience I’m writing for, you don’t know them yet. You can skip -learning them now. (Someday if you want to understand someone else’s -older macros, you can learn about them then.)
The syntax variation of them works similarly. The idea is, we’ll -define it to mean an error by default. Only inside of our -aif will it have a meaningful 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))
If we try to use it outside of an aif form, and +parameters in Racket:
> (define current-foo (make-parameter "some default value")) > (current-foo) "some default value"
> (parameterize ([current-foo "I have a new value, for now"]) (current-foo)) "I have a new value, for now"
> (current-foo) "some default value"
Historically, there are other ways to do this. If you’re +the target audience I’m writing for, you don’t know them yet. I +suggest not bothering to learn them, yet. (Someday if you want to +understand someone else’s older macros, you can learn about them +then.)
That’s a normal parameter. The syntax variation works similarly. The +idea is that we’ll define it to mean an error by +default. Only inside of our aif will it have a meaningful +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))
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 Other questions
Hopefully I will answer these in the course of the other sections. But diff --git a/main.rkt b/main.rkt index 4757097..8268f3e 100644 --- a/main.rkt +++ b/main.rkt @@ -694,7 +694,7 @@ convenient for formatting identifier names. #f)) } -It would be great to write: +You could write: @codeblock{ (aif (big-long-calculation) @@ -716,29 +716,40 @@ should be easy: (aif #t (displayln it) (void)) ] -Wait, what---@racket[it] is undefined? +Wait, what? @racket[it] is undefined? It turns out that all along we have been protected from making a -certain kind of mistake in our macros. The mistake is to introduce a -variable that accidentally conflicts with one in the code that is -using our macro. +certain kind of mistake in our macros. The mistake is if our new +syntax introduces a variable that accidentally conflicts with one in +the code surrounding our macro. -The Racket Reference -@hyperlink["http://docs.racket-lang.org/reference/syntax-model.html#(part._transformer-model)" "Section -1.2.3.5 Transformer Bindings."] has a good explanation of this, and an -example. (You can stop when you reach the part about set! -transformers.) Basically, the input syntax has "marks" to preserve -lexical scope. This makes your macro behave like a normal function. If -a normal function defines a variable named @racket[x], it won't -conflict with a variable named @racket[x] in an outer scope. +The Racket @italic{Reference} section, +@hyperlink["http://docs.racket-lang.org/reference/syntax-model.html#(part._transformer-model)" "Transformer +Bindings"], has a good explanation and example. Basically, syntax +has "marks" to preserve lexical scope. This makes your macro behave +like a normal function, for lexical scoping. -This makes it easy to write reliable macros that behave -predictably. Unfortunately, once in awhile, we want to introduce a -magic variable like @racket[it] for @racket[aif] on purpose. +If a normal function defines a variable named @racket[x], it won't +conflict with a variable named @racket[x] in an outer scope: -The way to do this is with @racket[define-syntax-parameter] and +@i[ +(let ([x "outer"]) + (let ([x "inner"]) + (printf "The inner `x' is ~s\n" x)) + (printf "The outer `x' is ~s\n" x)) +] + +When your macros also respect lexical scoping, it's easy to write +reliable macros that behave predictably. + +So that's wonderful default behavior. But @italic{sometimes} we want +to introduce a magic variable on purpose---such as @racket[it] for +@racket[aif]. + +The way to do this is with a "syntax parameter", using +@racket[define-syntax-parameter] and @racket[syntax-parameterize]. You're probably familiar with regular -parameters in Racket. +parameters in Racket: @i[ (define current-foo (make-parameter "some default value")) @@ -748,15 +759,16 @@ parameters in Racket. (current-foo) ] -@margin-note{Historically, there are other ways to do this. If you -know them, you will want to unlearn them. But if you're the target -audience I'm writing for, you don't know them yet. You can skip -learning them now. (Someday if you want to understand someone else's -older macros, you can learn about them then.)} +@margin-note{Historically, there are other ways to do this. If you're +the target audience I'm writing for, you don't know them yet. I +suggest not bothering to learn them, yet. (Someday if you want to +understand someone else's older macros, you can learn about them +then.)} -The syntax variation of them works similarly. The idea is, we'll -define @racket[it] to mean an error by default. Only inside of our -@racket[aif] will it have a meaningful value: +That's a normal parameter. The syntax variation works similarly. The +idea is that we'll define @racket[it] to mean an error by +default. Only inside of our @racket[aif] will it have a meaningful +value: @i[ (require racket/stxparam)