Hacker News new | ask | show | jobs
by kazinator 2035 days ago
The hygiene problem has multiple aspects. One issue is that the user's code can bind identifiers which the macro expansion expects to be predefined.

In a Lisp-1, we have to worry about this:

  (let ((list ...))
    (mac ...)) ;; expansion of mac wants to call (list ...)
Since the macro is not defining anything (its expansion is not introducing a binding for list), the ordinary gensym approach is not applicable.

THe problem persists into a Lisp-2 like Common Lisp, in this form:

  (flet ((list (...) ....)) ;; binding in function namespace
     (mac ...))             ;; wants to use (list ...)

Since local functions are rarer than local variables, and extra care can be taken in how hey are named, Lisp-2 mitigates the problem. Common Lisp goes a step further and makes it undefined behavior if code redefines a standard function (lexically or otherwise).

Hygienic macros solve this aspect of the issue as well. A hygienic (mac ...) can use (list ...) in its expansion. The hygiene mechanism ensures that this calls that list which is lexically visible at the macro definition (probably the global one in the library) even if the target site shadows list with its own definition.

1 comments

Again this is not fundamentally tied to whether functions and variables share a namespace, in clojure they do and yet your example above does not cause a problem. Watch:

Let's start out with a function:

   user=> (defn enlist [x] (if (list? x) x (list x)))
   #'user/enlist
   user=> (let [list 1] (enlist list))
   (1)
Create a (pointless) equivalent macro:

   user=> (defmacro enlist' [x] `(let [y# ~x] (if (list? y#) y# (list y#))))   
   user=> (let [list 1] (enlist' list))
   (1)
No hygiene and yet it works!

How? The secret is that backquote is automatically namespace qualifying:

    user=> '(list 1 2 3)
    (list 1 2 3)
    user=> `(list 1 2 3)
    (clojure.core/list 1 2 3)
In practice this + convenient gensym syntax (also demonstrated above) seems to be enough, just like in practice Common Lisp's approach of separate value and function cells for symbols + prohibition of rebinding function cells of symbols in the standard COMMON-LISP namespace is. Scheme has no cl style namespaces and no prohibition on redefining standard bindings and thus came up with a much more complex macro approach, at the gain of stronger hygiene guarantees (but see article above!) and the cost of an ugly design of considerable complexity that has a completely different sublanguage (non-orthogonally) baked in.

As far as purely hygiene is concerned that's IMO not a good trade-off at all; I could rattle off a long list of major usability gripes with both cl and clojure but problems caused by lack of macro hygiene wouldn't be on it.

Racket, which is basically several iterations beyond R*RS macros, OTOH is interesting because it's more principled approach gives you something qualitatively different to what you can do with clojure (no read-syntax control; bad error messages) and common lisp (readtables suck and break tooling; bad error messages).

I'd have to say that a ham-fisted read-time solution which throws important backquote identities under the bus is worse than just coding carefully under the threat of unhygienic macros, and worse than the complexity of hygienic macros.
What concrete problem do you see with it? [edit: BTW you can quite easily implement common-lisp style backquote in clojure as a macro, because just like scheme clojure does not share common lisp's defect of baking unquoting into the readtable dispatch of backquote; i.e. unquotes outside of backquotes are syntactically valid; '~x -> (unquote x)]