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 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 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—
There’s a bad way to do this and a good way.
The bad way is to use datum->syntax, which is tricky to use correctly. See Keeping it Clean with Syntax Parameters (PDF).
The good way 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"
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))
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 in local definition contexts like:
> (let ([it 10]) it) 10
or:
> (define (foo) (define it 10) it) > (foo) 10
For a deeper look, see Keeping it Clean with Syntax Parameters.