Arc Forumnew | comments | leaders | submitlogin
Macro Experiment: hygienic
4 points by fallintothis 2892 days ago | 1 comment
While playing around with the code for my profiler (, a pattern kept cropping up. I kept wanting to define a function f using another function g, then later redefine g while having f still work the same way. E.g.,

  arc> (def foo (x y)
         (isa x y))
  #<procedure: foo>
  arc> (def isa (x y)
         (prn "redefined!! <-- want to avoid this when calling foo")
         (is (type x) y))
  *** redefining isa
  #<procedure: isa>
  arc> (foo 'x 'sym)
  redefined!! <-- want to avoid this when calling foo
This was annoying, because while I could do the old closure trick

  arc> (let orig-isa isa
         (def foo (x y)
           (orig-isa x y)))
  #<procedure: foo>
  arc> (= isa 'something-dumb)
  arc> (foo 1 'int)
it involves manually changing all the uses of a particular function in the body of my definition. Plus, macros often expand into unforeseen function calls that I probably won't notice I have to replace when doing my manual intervention.

So, I got to wondering how I could do this automatically. Here's the macro I came up with:

  (mac hygienic exprs0
    (withs (exprs (map macex-all exprs0) ; see
            fns   (keep [and (isa _ 'sym) (bound _) (isa (eval _) 'fn)]
                        (dedup (flat exprs)))
            uniqs (map [uniq] fns))
      `(with ,(mappend list uniqs fns)
         ,@(map [do
                  (each (old new) (map list fns uniqs) 
                    (= _ (tree-subst old new _)))

    (def foo (x y)
      (isa x y)))
expands every macro and (rather un-delicately) replaces any symbol in the (def ...) that's a function name with a lexically-bound gensym, giving us

  (with (gs1751 sref gs1752 bound gs1753 disp gs1754 stderr gs1755 isa)
    ((fn ()
       (gs1751 sig '(x y) 'foo)
       ((fn ()
          (if (gs1752 'foo)
              ((fn ()
                 (gs1753 "*** redefining "
                 (gs1753 'foo (gs1754))
                 (gs1753 #\newline (gs1754)))))
          (assign foo
                  (fn (x y) (gs1755 x y))))))))
In action:

  arc> (hygienic (def foo (x y) (isa x y)))
  #<procedure: foo>
  arc> (foo 'x 'sym)
  arc> (= isa 'something-dumb)
  arc> (foo 'x 'sym)
  t ; look ma, no error!
Unfortunately, function definitions still get in the way (and I don't think there's much we can do about it). In this example, since isa is defined by

  (def isa (x y) (is (type x) y))
we can still clobber is and foo will be none-the-wiser:

  arc> (hygienic (def foo (x y) (isa x y)))
  #<procedure: foo>
  arc> (let old-isa isa
         (def reset () (= isa old-isa))
         (= isa 'something-dumb))
  arc> (foo 'still 'works)
  arc> (reset)
  #<procedure: isa>
  arc> (= is 'something-dumb)
  arc> (foo 'doesnt 'work)
  Error: "Function call on inappropriate object something-dumb (sym work)"
Fun experiment, though.

1 point by akkartik 2892 days ago | link

Very cool. I've been thinking a lot about when you want to reflect changes in bindings and when you don't care. I was reminded, for example, of this thread from 2 months ago: