[racket] Macros baffle me

From: Sean Kanaley (skanaley at gmail.com)
Date: Fri May 2 11:57:59 EDT 2014

Indeed you are correct.  I thought about exactly *why* it's bad to use
datum->syntax, and it can lead to the kind of behavior I have in the below
example:

#lang racket
(define X 100)
(define y 0)
(define-syntax (set-y stx)
  (syntax-case stx ()
    [(_ n) #`(set! #,(datum->syntax #'X 'y) n)]))
(let ([y #f])
  (set-y 50)
  y)
y

Will return #f and 50 instead of 50 and 50.  The set-y call inside the let
accidentally used #'X's context (the top level) and so set the y defined
there as (define y 0).  Had it been written as #,(datum->syntax stx 'y),
using the local context where it was called ("stx", from inside of the let
where y is shadowed), it would have set the inner y and the result would be
50 and 0.  Which one is truly correct is up to the macro writer of course,
but since hygienic macros are intended to preserve scope the way lambdas
do, it's probably better to not use datum->syntax where "normal" behavior
is desired.  So Robby's solution is more correct in this sense.  And as the
creators have blogged elsewhere, even when hygiene is seemingly being
broken (anaphoric if), it can still be handled without datum->syntax
through syntax parameters.

Obviously they know everything I just said and more, so I say this as an
addendum to my solution to anyone who might be reading it and get the wrong
idea!  datum->syntax is inherently unsafe!  Does anybody have a simple
example where it's especially helpful/required?


On Fri, May 2, 2014 at 9:37 AM, Robby Findler
<robby at eecs.northwestern.edu>wrote:

> I don't think you want to use syntax->datum or datum->syntax in this
> macro. Here's another way to write it.
>
> Robby
>
> #lang racket
> (define-syntax-rule
>   (with-vars (vars ...) x)
>   (for*/and ([vars (in-list '(#t #f))]
>              ...)
>     x))
>
> (define-syntax-rule (then x y) (implies x y))
> (define equiv equal?)
>
> (define-for-syntax (collect-fvars stx exp)
>   (let loop ([exp exp])
>     (define (collect stx-lst)
>       (apply append (map loop (syntax->list stx-lst))))
>     (syntax-case exp (or then and not)
>       [(or x ...) (collect #'(x ...))]
>       [(and x ...) (collect #'(x ...))]
>       [(then x y) (collect #'(x y))]
>       [(equiv x y) (collect #'(x y))]
>       [(not x) (collect #'(x))]
>       [x
>        (identifier? #'x)
>        (list #'x)]
>       [_ (raise-syntax-error #f "unknown expression" stx exp)])))
>
> (define-syntax (tauta stx)
>   (syntax-case stx ()
>     [(_ exp)
>      #`(with-vars #,(collect-fvars stx #'exp) exp)]))
>
> (tauta (or (then P Q) (then Q (not P))))
> (tauta (or P (not P))) ; excluded middle
> (tauta (or (and P Q) (or (not P) (not Q))))
> (tauta (equiv (then P Q) (then (not Q) (not P)))) ;equiv
> (tauta (then (and (then (not A) B) (then (not A) (not B))) A))
> ;reductio ad absurdum
> (tauta (equiv (not (and A B)) (or (not A) (not B)))) ; De Morgan's law
>
> On Fri, May 2, 2014 at 8:26 AM, Sean Kanaley <skanaley at gmail.com> wrote:
> > Here's a solution (at the bottom) with the datum/syntax converters.
>  flatten
> > is built-in, free-vars is basically filtering stuff and then removing
> > duplicates, with-vars can take advantage of the built-in nested for, and
> > then tauta.
> >
> > tauta can get the "quoted" form of the expression like when you used
> eval by
> > calling syntax->datum on the syntax given to tauta, minus "tauta" itself,
> > which is the x pattern.  But the pattern itself is not a syntax object so
> > requires the syntax quote "#'", at which point you can de-syntaxify it
> with
> > syntax->datum, getting '(or (and P Q) etc.).  You can try this in the
> REPL,
> > just do (syntax->datum #'(or (and P Q) etc.)).
> >
> > Once free vars has this as '(P Q), map this list by re-syntaxifying the
> > symbols with datum->syntax.  The first parameter to datum->syntax
> basically
> > is the key to "breaking" hygiene lisp-style.  We want to say that the
> free
> > variable symbols in "x" are the *same* as in our new datum->syntaxified
> > symbols that get passed to with-vars to bind in the for/and, so we say
> they
> > ultimately belong to the same lexical context (your top level code) by
> > passing the same syntax ("stx") that tauta was called in.  In other
> words,
> > those generated vars are created as if you yourself defined them in the
> > module.  Without passing stx to datum->syntax, it would be as if someone
> > else defined them elsewhere but coincidentally with the same names, and
> for
> > safety hygienic systems disallow this shadowing (their x and your x are
> more
> > like x1 and x2, being secretly renamed by the system).  So we tell it we
> > really mean our own code's context.  In general, you can break hygiene
> this
> > way by using whichever syntax object refers to wherever you want to
> pretend
> > the syntax came from.
> >
> > #lang racket
> > (require (for-syntax racket))
> >
> > (define-syntax-rule (with-vars (vars ...) x)
> >   (for*/and ([vars (in-list '(#t #f))]
> >              ...)
> >       x))
> >
> > (define-for-syntax (free-vars xs)
> >   (define (keyword? x)
> >     (member x '(and or then -> <-> not
> >                     equiv display displayln)))
> >   (remove-duplicates
> >    (filter (compose not keyword?)
> >            (flatten xs))))
> >
> > (define (then x y) (or y (not x)))
> > (define (equiv x y) (and (then x y) (then y x)))
> >
> > (define-syntax (tauta stx)
> >   (syntax-case stx ()
> >     [(_ x)
> >      (with-syntax ([(vars ...) (map (λ (var) (datum->syntax stx var))
> >                                     (free-vars (syntax->datum #'x)))])
> >        #'(with-vars (vars ...) x))]))
> >
> > ____________________
> >   Racket Users list:
> >   http://lists.racket-lang.org/users
> >
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.racket-lang.org/users/archive/attachments/20140502/c3f40198/attachment.html>

Posted on the users mailing list.