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—such as it for aif.

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.