Arc Forumnew | comments | leaders | submitlogin
Macro Experiment: hygienic
4 points by fallintothis 4415 days ago | 1 comment
While playing around with the code for my profiler (http://www.arclanguage.org/item?id=16090), 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
  t
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)
  something-dumb
  arc> (foo 1 'int)
  t
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 bitbucket.org/fallintothis/macdebug
            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 _)))
                  _]
                exprs))))
Thus,

  (hygienic
    (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 "
                         (gs1754))
                 (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)
  t
  arc> (= isa 'something-dumb)
  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))
  something-dumb
  arc> (foo 'still 'works)
  nil
  arc> (reset)
  #<procedure: isa>
  arc> (= is 'something-dumb)
  something-dumb
  arc> (foo 'doesnt 'work)
  Error: "Function call on inappropriate object something-dumb (sym work)"
Fun experiment, though.


1 point by akkartik 4414 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: http://arclanguage.org/item?id=15587

-----