Arc Forumnew | comments | leaders | submitlogin
(un)hygienic macros
6 points by simonb 6174 days ago | 9 comments
Looking at macros in arc.arc or any CL code for that matter, they are full of boiler-plate aimed at preventing variable capture. Wouldn't it therefore be in the spirit of solving for the most common case to provide hygienic macros along with their grimy brethren?


2 points by icemaze 6174 days ago | link

Boilerplate code? You mean all the commas? Please compare:

  (define-syntax unless
    (syntax-rules ()
      ((unless condition body ...)
       (if (not condition) (begin body ...)))))
with (Common Lisp):

  (defmacro unless (condition &body body)
    `(when (not ,condition)
       ,@body))
Unhygienic is shorter byte-wise and token-wise. But they are not so different, are they? If you need to prevent variable capture for some symbols, you need one more line with with-gensyms and the symbols to protect. Easy enough.

Otherwise, please provide some real code to talk about.

-----

3 points by simonb 6174 days ago | link

To make it perfectly clear, I am not advocating Scheme's implementation of hygienic macros. I am merely pointing out with-gensyms is such a common pattern it would make sense to abstracted it away. Furthermore we already have such an abstraction, it is called hygienic macros.

In a language where commonly used names are abbreviated to save a character or two, "one more line" of boiler-plate should not be accepted at face value.

Ideally hygienic macros would look exactly the same as unhygienic with some clever transformations behind the scenes. For instance instead of

  (mac complement (f)
    (let g (uniq)
      `(fn ,g (no (apply ,f ,g)))))
one would write:

  (hmac complement (f)
    `(fn g (no (apply ,f g)))))
and macro hmac would do the rest (probably arriving at something resembling the former).

-----

2 points by icemaze 6174 days ago | link

Oh ok. Now I understand your point better.

My points:

1. with-gensyms (or w/uniq in Arc) is not so common. I recently wrote a (small) application in Common Lisp and less than 50% of the macros need it. Same thing for once-only.

2. Yes, it would be a great thing to abstract that boilerplate away... if it was possible. I think Scheme developers aren't stupid. If it were easy to make a macro system that works like yours, it would probably already exist. As it's been argued, hygienic macros tend to make variable-capture much harder when you need it. And sometimes you need it a lot (e.g. DSL). Again: in my application, about 40% of macros capture a symbol, usually to make things shorter.

-----

2 points by elibarzilay 6174 days ago | link

Off topic, but:

* The define-syntax...syntax-rules boilerplate is a very-easy-to-bypass thing.

* Small applications are not a good justification for unhygienic macros: they bite much more when there are several people involved, or when a project lives for a long time.

* Adding automatic gensyms is not enough to call the result hygienic.

-----

1 point by icemaze 6174 days ago | link

1. I didn't know that, I never managed to get past that point. :P Are there macros like with-gensyms that make your life easier? Could you give a reference to show more precisely what you mean? That would be really nice.

2. What I mean is that variable capture is even more useful in big projects (because the DSL approach tends to be more powerful). It is true that they could cause nastier bugs if used incorrectly. But if you use them correctly (by forcing an order of evaluation and protecting the variables you use) there should be no problems. That's certainly true for a LISP-2, I'm not sure about Arc.

3. I don't care how you call it: if you use gensyms where needed the resulting macros won't screw you up! That's what matters, doesn't it?

-----

3 points by elibarzilay 6174 days ago | link

1. You complained about scheme having too much boilerplate code. This is easy to fix, for example, with this macro-defining-macros:

  (define-syntax defmac
    (syntax-rules ()
      ((defmac (name x ...) body)
       (define-syntax name
         (syntax-rules () ((name x ...) body))))))
you can write:

  (defmac (unless condition body ...)
    (when (not condition) body ...))
(See also item 4.)

2. Yes, DSLs are even more useful in big projects; obviously, the only source of all problems is using something incorrectly, the problem is how easy it is to write something incorrect; evaluation order has nothing to do with macros; protecting variables you use doesn't cover all problems; lisp-2 is "statistically better" because people shadow function names less frequently (see also next item).

3. They will bite -- even if you always use gensyms. A simple example:

  (def twice (func) [func (func _)])
  (mac with-duplicate (func . body)
    `(let ,func (twice ,func) ,@body))
This looks simple enough -- but `with-duplicates' happily inserts a `twice' symbol, regardless of what it happens to be bound to:

  (let twice [+ _ _] (with-duplicate cdr (cdr '(1 2 3 4))))
4. Extra point: doing simple captures in scheme with `define-syntax' is said to be difficult in the general case, but it is possible to make that easy too. For example, see the macro definition and examples in http://tmp.barzilay.org/defmac.ss -- the definition uses mzscheme's `syntax-case' etc, but this is not something that you should know about to use it. Just skip to the example to see how it works. (Cheat: there are subtle cases that this doesn't work.)

-----

1 point by icemaze 6173 days ago | link

1. Yes, that's much nicer, thanks.

2. "the problem is how easy it is to write something incorrect". Very true.

"evaluation order has nothing to do with macros" I'm sorry but that's incorrect. An example will show you why:

  (mac sum (x y) `(+ ,y ,x)) ; This is a toy, but some macros suffer from the same problem without being so trivial/stupid
  (= x 1)
  (+ x (++ x))   ; -> 3, the expected result
  (= x 1)
  (sum x (++ x)) ; -> 4, wrong result
Common Lisp macros usually solve this with a meta-macro called once-only that forces things to be evaluated in the right order and only once. Google it for more info.

"lisp-2 is statistically better". Well, not if people know that shadowing function can cause problems. Now a question arises: does shadowing functions solve problems in a way not possible with other methods? Does shadowing function make programs shorter? Or, equivalently, is shadowing functions really that useful? (Please provide examples if you answer this). If not, using a lisp-2 and being careful is enough. But yes, it's easy to overlook a symbol and make a mistake that can be costly. On that, I agree.

3. You are right and that's why I said "I'm not sure about [a lisp-1 like] Arc".

4. That seems really interesting, I'll look into it.

-----

1 point by elibarzilay 6173 days ago | link

You didn't understand me.

One of the things that you can do with macros is specify evaluation order, that's correct. You should also know when and how to evaluate your arguments, as done with the once-only utility (which is really just a thin wrapper around the obvious solution of binders).

But when I said "evaluation order has nothing to do with macros" I meant the kind of macro facility that you use (the original context of this thread was hygiene). No matter which kind of macros you have, the above issues are still the same.

-----

2 points by randallsquared 6174 days ago | link

I don't think it would resemble the former, if only because you'd want to use uniq calls for functions and macros in the expansion, lest they be redefined between here and the use site.

-----