clojure quotes and tilde in macros

I am new to Clojure and I am having trouble understanding its quoting system. I am writing a macro and I have made two similar cases - one works, the other doesn't. In a sense, I am just trying to surround my statement with try/catch condition.

Here's the code that works:

(defmacro safe
  [arg1 arg2]
  (list 'let arg1 arg2)
)

Here's the code that doesn't work

(defmacro safe
    [arg1 arg2]
    '(try
        ~(list 'let arg1 arg2)
        (catch Exception e (str "Error: " (.getMessage e)))
    )
)

after the ~ symbol, it should escape the quotes, but for some reason, it seems like it doesn't. The Error is: "Unable to resolve symbol: arg1 in this context...".

Thanks for your help!


Edit:

Code that I call the macro with:

(println (safe [s (new FileReader (new File "text.txt"))] (.read s)))

Also, I import this:

(import java.io.FileReader java.io.File)

Goal is to read first symbol from a file, while being safe from incorrect text file names. This is my school assignment by the way, so I should not use any other way to do it and the macro has to be called like that, I know about the with-open , etc.


Escaping ( ~ ) only works with quasi-quote (also known as syntax quote). You need to use the "back-quote" ( ` , found on the same key as ~ on most US keyboards), not the normal single-quote ( ' , which is on the same key as " ). It's a subtle difference graphically, which can be easy to miss.

You can also get rid of the list by not quoting the let and un-quoting arg1 and arg2 . With these changes, we get something like this:

`(try ;; note back-quote, not regular quote.
    (let ~arg1 ~arg2) ;; Getting rid of list — not necessary, but most find this more readable
    (catch Exception e (str "Error: " (.getMessage e))))

Now, if we check our progress using macroexpand :

(macroexand '(safe2 [s (new FileReader (new File "text.txt"))] (.read s)))

We get the following:

(try (clojure.core/let [s (new FileReader (new File text.txt))]
       (.read s))
     (catch java.lang.Exception user/e
       (clojure.core/str Error: (.getMessage user/e))))

You might notice that in Clojure, quasi-quoted symbols are resolved when the macro is compiled. Symbols that can't be resolved are qualified with the current namespace ( user in this case). The rationale for this is that it helps you write "hygienic" macros. In this case, however, we don't want to resolve the e symbol, because local variables can't be given a qualified name.

We have a few options now. The first is to basically forgo hygiene. That works in this case, since you aren't expanding any user-provided code inside the catch block. So there isn't any way the name e could conflict with a user variable. This solution would look like this:

`(try
    (let ~arg1 ~arg2)
    (catch Exception ~'e (str "Error: " (.getMessage ~'e))))

Note the use of ~'e instead of just e . The ~ is to escape from the quasi-quote, then we use a regular quote to quote e . It looks a little weird, but it works.

While the above solution works, it's arguably better to use a generated symbol instead of e . That way, if you ever change your macro to accept user-provided code for the catch block, you can be sure it will still work. In this particular case, an "auto-generated" symbol fits the bill perfectly. This looks as follows:

`(try
    (let ~arg1 ~arg2)
    (catch Exception e# (str "Error: " (.getMessage e#))))

Basically, whenever the Clojure reader encounters a symbol with a trailing # inside a quasi-quote form, it will produce a new gensym 'd symbol and replace every occurence of that symbol (ie, e# ) with the gensym 'd one. If we macroexpand this, we would get something like:

(try (clojure.core/let [s (new FileReader (new File text.txt))]
       (.read s))
     (catch java.lang.Exception e__66__auto__ 
       (clojure.core/str Error: (.getMessage e__66__auto__))))

As you can see, every occurence of e# was replaced with a machine-generated symbol. Here e__66__auto__ is the auto-gen'd symbol.

Finally, while auto-gen is convenient, it is not always sufficient. The main issue is that, since auto-gen'd symbols are produced at read time, every evaluation of the quasi-quote form (ie, expansion of the macro) will use the same auto-gen'd symbols. In this paricular case, that's fine. However, there are cases where this can lead to conflicts if, for example, nested macro forms are used. In these cases, it is necessary to use an explicitly gensym 'd symbol every time the macro is expanded. With this approach, the body of your macro would look like this:

(let [e (gensym)]
  `(try
     (let ~arg1 ~arg2)
     (catch Exception ~e (str "Error: " (.getMessage ~e)))))

Here e is a local variable in your macro, whose value is the a fresh symbol (via gensym ). In the quasi-quote, we must escape e in order to use that gensym 'd value.

If we expand this, we would get something like:

(try (clojure.core/let [s (new FileReader (new File text.txt))]
       (.read s))
     (catch java.lang.Exception G__771 
       (clojure.core/str Error: (.getMessage G__771))))

If we expand this again, we would find G__771 replaced by a different symbol (perhaps G__774). In contrast, the auto-gen'd solution ( e# ) would always use the same symbol for every expansion (at least until we recompile the macro).

Hopefully this gives you a better understanding of macros, symbols, and hygiene. Let me know if anything isn't clear.


There's two problems here:

First, unsplicing (~ and ~@) work only inside syntax-quote (`). Syntax quote is usually chosen for macros because it also does symbol namespace resolution at macro definition place. Simple quote (') will keep symbols intact, so ns resolution will happen at macro invocation site. Because you do not control where and how your macro will be called, it might be very confusing.

Second, you cannot just declare new symbols in quoted code, it may cause name conflict with code around macro. Each new symbol introduced by macro should use suffix # so Clojure macroexpansion will replace it with new autogenerated name that cannot cause any name conflicts with user code.

(defmacro m []
 `(let [x# 1]
    x#))

(macroexpand-1 '(m)) =>

=> (clojure.core/let [x__6257__auto__ 1]
     x__6257__auto__)

Note how let became fully-qualified clojure.core/let (avoiding ns resolution nuances later), and x# got replaced with x__6257__auto__ (avoiding name conflicts).

Your code should be written as this:

(defmacro safe [arg1 arg2]
 `(try
    (let ~arg1 ~arg2)
      (catch Exception e#
        (str "Error: " (.getMessage e#)))))

Check like this:

(macroexpand-1 '(safe [s (new FileReader (new File "text.txt"))] (.read s)))

↓↓↓

(try
  (clojure.core/let [s (new FileReader (new File "text.txt"))]
    (.read s))
  (catch java.lang.Exception e__6283__auto__
    (clojure.core/str "Error: " (.getMessage e__6283__auto__))))

I would also recomment using idiomatic names for macro args and make second argument of arbitrary lenght:

(defmacro safe-let [bindings & body]
 `(try
    (let ~bindings
      ~@body)
      (catch Exception e#
        (str "Error: " (.getMessage e#)))))
链接地址: http://www.djcxy.com/p/65762.html

上一篇: 在Clojure宏中引用基于〜和〜@的选择

下一篇: clojure引号和宏代码中的代字符