[racket] Another macro question: making a lexically binding keyword?

From: Eli Barzilay (eli at barzilay.org)
Date: Wed May 2 02:38:34 EDT 2012

Three hours ago, Danny Yoo wrote:
> >
> > there is no breakage here: the way that your `interfere' is broken
> > is essentially the feature that syntax parameters provide: making
> > it possible to have new forms that will adjust the meaning of
> > `INDEX'.  If you don't want that, then you can use the syntax
> > equivalent of the above fixed `interfere'.
> I must state up front: I agree with you that the behavior of syntax
> parameters is valid.  I am not saying at all that I think syntax
> parameters need to be changed at all.  I'm not sure if I made that
> clear, and apologize for that confusion.

To clarify, all I'm saying is that this supposed breakage with syntax
parameters is not something that can be fixed, since it's the main
feature that they get you.  So any such fix is should look like: if
you don't want that feature, then use them...  (And this is the same
with parameters: the main property is that changes are visible outside
of the function, and if that's not what you want then you shouldn't
use parameters.)

The rest will expand on this, which will be long...  Sorry.

> Rather, I would like to know what thing I should be using instead,
> so that the situations I'm constructing behave according to the test
> cases I've constructed.

I don't know which tests you have, but:

> I want to understand how to make the uses of INDEX know how to pair
> up lexically with uses of 'ranged'.

if that's the *only* thing that you want, then using an unhygienic
macro sounds obvious...  Note that you're even saying that you want to
pair uses with `ranged' which makes it obvious which lexical scope
should be used in your `datum->syntax'.

But again, that depends on what you want...  Using this unhygienic
identifier does exactly that kind of match, which means that you can't
write macros that abstract over `ranged' -- since the `INDEX' binding
will be visible in the macro but not in its uses, and this is IMO
unfortunate since these kinds of abstractions are (IME, of course)
things that people very commonly want.  (Eg, the anaphoric `if-it'
story has a `cond-it' abstraction as the classic example of where
things go bad.)

Perhaps this example would work better:

  (define-syntax-rule (5-times E) (ranged 5 E))
  (define-syntax-rule (interfere E) (ranged 1 E))
  (5-times (interfere (displayln INDEX)))

If you use syntax paremters in the simple way, that `interfere' is
buggy.  If you fix that bug, you inevitably break `5-times'.  Either
way you end up with the last line not doing what you want.

Ultimately, there are three regions of code involved: the definition
of `ranged', the definition of `interfere', and the end user code.
Plain hygiene makes `INDEX' available only in the first.  The obvious
unhygienic construction makes it available in the first and the third.
Syntax parameters make it available in all three -- but in case of
(ranged 5 (interfere ...)) this means that interfere's use is visible
in its body.  IMO (I can't do more than "IMO"...) *this* behavior is
what I'd expect -- the analogy with an anaphoric `if-it' is that
you're basically creating a new anaphoric tool that *builds on*
`if-it', and it's therefore a feature that it gets to rebind `it' to
make this particular composition work.

Now, if there are cases where you want to use `ranged' but you want
this use to be *invisible* to users of your `interfere' macro, then
you need to hide this fact.  The unhygienic `datum->syntax' is doing
that, but it hides it too well, leaving it visible only in actual uses
of `ranged' and nowhere else.  Another "solution" is to do nothing and
document your `interfere' as something that changes the meaning of
`INDEX' -- it's inconvenient but it doesn't make it unusable -- users
of `interfere' can do something like:

  (5-times (let ([INDEX INDEX]) (interfere (displayln INDEX))))

The impractical downside is that abstractions on `interfere' need to
be similarly documented...  A proper solution that makes `interfere'
really be a no-op is the analogy to parameters that I mentioned

  (define-syntax (interfere stx)
    (syntax-case stx ()
      [(_ E) (let ([outer-index (syntax-parameter-value #'INDEX)])
               #`(ranged 1 (syntax-parameterize ([INDEX #,outer-index])

And if that seems heavy on the meta side, then you can do something
similar with a simple rewrite too:

  (define-syntax-rule (interfere E)
    (let ([thunk (λ () E)]) (ranged 1 (thunk))))

If this was something that is needed in many cases, then it looks
pretty easy to put one of these in some helper macro, but I've never
seen even a single practical case where it was needed.

Finally, two more observations:

1. Note that `interfere' is a bad name -- it's like an argument
   against lexical scope that is demonstrated using a function called
   `broken'...  If you use something like `if-it' and `cond-it' then
   the discussion makes more sense.

2. It's also interesting to note the "screw you guys, I'm going home"
   solution of unhygienic macros: you flatten all scopes to a single
   big happy scope, and the problem just goes away by itself.  (And
   that's oversimplifying, since you also have gensyms as an attempt
   to get half of the sanity back, and fexprs to get the other half
   too, at a questionable cost...)

          ((lambda (x) (x x)) (lambda (x) (x x)))          Eli Barzilay:
                    http://barzilay.org/                   Maze is Life!

Posted on the users mailing list.