[racket] On Cooperating Macros

From: Ryan Culpepper (ryan at cs.utah.edu)
Date: Fri Nov 16 18:05:12 EST 2012

On 11/16/2012 05:16 PM, Ray Racine wrote:
> I've run across a few things mentioning cooperating macros, something
> I'm working through.
> Consider the following in a source file "nest.rkt"
> https://gist.github.com/4090877
> So in this example each expansion of the two defined macros in the
> `nest' module sees mutually mutable state at meta level 1.
> And of course in a separate source module, say "bird-watcher.rkt" with
> the following.
> #lang racket
> (require "nest.rkt")
> (define the-nest
>     (begin
>      (daddy-brings-worms)
>      (daddy-brings-worms)))
> the-nest
> ;; end-of-module
> One sees that the mutable state is not maintained across modules.  i.e.,
> Not cooperating.  All, I'm guessing, a direct consequence of the
> "Immutable Laws Of Flatt" (ILOF), "You Want It When", and "Modules,
> Visitations and Instantiations, Oh My".

Yes. To summarize: the expansion/compilation of each module starts with 
a fresh state, which is then initialized with the compile-time parts of 
the modules it depends on.

> BUT, the ILOF has a backdoor[1], there exists a meta level wormhole
> which allows state to tunnel through modules and expansions.  If one
> were to use `define-syntax' at compile time (during meta level 1) to
> bind a non-procedure value, one can rat-hole state which is subsequently
> accessible via `syntax-local-value' in an a different macro expansion at
> meta level 1, in a different module. i.e., the macros now can cooperate.
>   Here I'm taking "cooperate" as synonymous with shared mutable state.
> Based on what I think I understand, I can rejigger my bird net examples
> to cooperate using the wormhole.

No, that's not a back door, because you still get a fresh state and then 
you execute the definition again. So if you did something like

   (define-syntax the-nest-state (box (cons 0 0)))

and then mutated the box during the expansion of module A, then when you 
start expanding module B you execute that syntax definition again, so 
module B starts out with a fresh box holding (cons 0 0).


The *only* way for module A to affect the compilation of module B in any 
way is for module B to require (directly or indirectly) module A.

And the only way to make a compile-time side-effect in module A 
"persistent", so it will affect the compile-time state of other module 
compilations is to put it in a 'begin-for-syntax'. (Actually, there are 
a few other ways, but they take longer to explain.)


I'll stop here in case you want to reformulate the questions in your 
next few paragraphs based on my answers so far.


> So assuming the above is more-or-less correct, can cooperating macros be
> accomplished solely via the application of module require / provide at
> proper meta levels.  One one hand, I'm visualizing a `define-syntax'
> binding a non-procedure value at compile time (meta 1) as in essence
> pushing the value up into meta 2.  And the `syntax-local-value' as
> reaching out from meta 1 into meta 2 to fetch the state down.  It's
> global because meta layer 2 is a single global environment in which meta
> 1 syntax transformers are evaluated.
> On the other, other hand I visualize that during module visitations and
> instantiations the _only_ thing which can _cross_ meta levels is syntax.
> i.e. syntax at meta level N can be lifted into meta level N+1,
> transformed and returned back to meta level N.  But as only syntax
> crosses meta boundaries, hence the define-syntax and syntax-local-value
> backdoor.
> Or another way of phrasing the question, "Can one
> write cooperating macros without resorting to the use of `define-syntax'
> and `syntax-local-value'".   If yes, how?
> Thanks,
> Ray
> [1] Seems the only true immutable law in CS is, there's always a backdoor.
> ____________________
>    Racket Users list:
>    http://lists.racket-lang.org/users

Posted on the users mailing list.