Arc Forumnew | comments | leaders | submitlogin
Macros without 'mac'?
4 points by ulix 5500 days ago | 7 comments
I tried thinking about writing without mac (with plain def) the first macro example from the tutorial:

(def when (test­ . body)­ (eval­ `(if ,test­ (do ,@bod­y))))

(just a matter of adding an 'eval') which at first glance it looks to work as a macro, provided that you care to quote (and if necessary un-quote) some arguments at calling time:

(when 1 (pr "hello ") 2) 'mac' version (when 1 '(pr "hello ") 2) 'def' version

and for the second macro example,

(let x "blub " (repeat 3 (pr x))) 'mac' version (let x "blub " (repeat 3 `(pr ,x))) 'def' version

Is this 'def'+'eval' macro way fully equivalent to the analogous 'mac' definition? (I'm a one hour Arc newbie, so please forgive the possible trivial question)

Best regards

Mario



5 points by waterhouse 5500 days ago | link

Due to the way eval works, it doesn't touch lexical variables. So:

  arc> (let i 2 (+ i 1))
  3
  arc> (let i 2 (eval '(+ i 1)))
  Error: "reference to undefined identifier: _i"
This isn't specific to Arc, by the way. In Racket and in Common Lisp, it doesn't work:

  > (let ((i 2)) ;Racket
      (eval 'i))
  . . reference to undefined identifier: i
Thus, your version of when will not work the same way as the macro version when you try to do this, for example:

  arc> (time:for i 1 1000 (mywhen '(is i 666) '(prn "Oh no!")))
  Error: "reference to undefined identifier: _i"
Other than that little kink with lexical variables, though, using eval does do pretty much the same thing as using a macro.

One more thing: eval is also much less efficient if you intend to use it repeatedly. By definition, eval performs full syntax analysis every time you use it. In the below example, its argument is a constant, so a smart compiler could theoretically optimize it to run like normal code, but in general its argument would not be constant.

  arc> (= i 0)
  0
  arc> (time:repeat 1000 (++ i))
  time: 2 msec.
  nil
  arc> (time:repeat 1000 (eval '(++ i)))
  time: 145 msec.
  nil
So, I recommend against using eval if you can avoid it. One use case is where you wait for the user to type in an expression, and you evaluate it and print the result.

-----

1 point by ulix 5499 days ago | link

Thank you for the answer, very interesting. Here's some more things I found:

1) actually the "reference to undefined identifier" error can be avoided un-quoting the variables at calling time, as in: (let i 2 (eval­ `(+ ,i 1))) (time:for i 1 1000 (mywh­en `(is ,i 666) '(prn­ "Oh no!")))

2) the last expression is computed in 176 msec, whereas the regular macro 'when' is computed in 2 msec (much faster as you forecasted).

3) these examples don't work in online interpreter Arc Web REPL (http://dabuttonfactory.com:8080/), but they work in TryArc (http://tryarc.org/)

Thanks

-----

2 points by aw 5500 days ago | link

Btw, you need an additional quote to get values like your "x" into the eval in all cases... "blub " works because a literal string evaluates to itself, but consider a list like (+ 1 2)

  arc> (let x '(+ 1 2) (repeat 3 (pr x)))
  (+ 1 2)(+ 1 2)(+ 1 2)nil
  arc> (def xrepeat (n . body)
         (eval `(for ,(uniq) 1 ,n ,@body))))
  #<procedure: xrepeat>
  arc> (let x '(+ 1 2) (xrepeat 3 `(pr ,x)))
  333nil
This can be fixed by adding another quote...

  arc> (let x '(+ 1 2) (xrepeat 3 `(pr ',x)))
  (+ 1 2)(+ 1 2)(+ 1 2)nil
Also, you got around the limitation that eval doesn't have access to the lexical variables of its caller by passing in the value of "x", but if you wanted to have your eval/macro to set the value of a variable, that would be harder.

As waterhouse mentioned, two differences between macros and eval are that macros are expanded once when your program is loaded, but eval has to expand and compile its argument every time its called. And like any function eval doesn't have access to the lexical variables of its caller unless you pass in values yourself.

Aside from that, macros and eval are quite similar in the sense that they both treat code as data, and so you have the opportunity to manipulate the code before it gets evaluated.

-----

1 point by ulix 5499 days ago | link

You can get around the problem also in the following "weird" way:

(let x ''(+ 1 2) (xrepeat 3 `(pr ,x)))

but the way you proposed is more "natural" since it respects the "rule" of quoting functions and variables passed at calling time.

Am i wrong if I think that, in the end, the main difference between 'def'+'eval' and 'mac' is that 'mac' allows its code to be evaluated and expanded once, whereas 'def' needs to expand its code every time it is called ?

-----

2 points by rocketnia 5499 days ago | link

Am i wrong if I think that, in the end, the main difference between 'def'+'eval' and 'mac' is that 'mac' allows its code to be evaluated and expanded once, whereas 'def' needs to expand its code every time it is called ?

It's 'eval that expands its code once every time it's called. If you don't use 'eval at all, then all your arc code will be expanded exactly once, at the time you enter it at the REPL or load it from a file (since those are the points where 'eval is called internally).

When you enter the expression (def foo (x) (obj key x)) is at the REPL, it's evaluated as a two-step process: First it's compiled (which expands the (def ...) and (obj ...) macro forms), and then the compiled code is run. When it's run, the body of the function isn't run yet; instead, it's packed up into a procedure and stored as foo. When you call foo later, no expansion takes place, only running of already compiled code.

A better (but not perfect) way to use 'eval equivalently to 'mac is something like this:

  (def mywhen (test . body)
    `(if ,test (do ,@ body))
  
  (eval `(time:for i 1 1000 ,(mywhen '(is i 666) '(prn "Oh no!"))))
If we wanted to make 'do a non-macro, we could do that too:

  (def mydo body
    `((fn () ,@body)))
  
  (def mywhen (test . body)
    `(if ,test ,(apply mydo body)))
  
  (eval `(time:for i 1 1000 ,(mywhen '(is i 666) '(prn "Oh no!"))))
Finally, if we wanted to avoid the use of macros altogether, the example would turn into something like this:

  ; The procedure corresponding to an Arc macro can already be obtained
  ; using 'rep.
  (assign mywhen rep.when)
  (assign mytime rep.time)
  (assign myfor rep.for)
  
  (eval:mytime:myfor 'i '1 '1000 (mywhen '(is i 666) '(prn "Oh no!")))
I could make a few guesses as to why this kind of programming isn't popular in lisps, but my personal reason is that I tend to consider all operators to provide their own syntax, with procedure call syntax just being the default choice. For me, it's inconsistent to have the '(prn ...) syntax be quoted when the (mywhen ...) syntax isn't.

-----

1 point by ulix 5498 days ago | link

Very interesting, thanks!

Do you know where I can find an Arc language reference?

-----

1 point by aw 5498 days ago | link

http://arcfn.com/doc/index.html and http://arcfn.com/2009/06/whats-new-in-arc3.html

-----