转义 (~) 仅适用于准引号(也称为语法引号)。您需要使用“反引号”(`,与大多数美国键盘上的 ~ 在同一个键上),而不是普通的单引号(',与 @987654325 在同一个键上@)。这是图形上的细微差别,很容易被忽略。
您也可以通过不引用 let 和取消引用 arg1 和 arg2 来摆脱 list。通过这些更改,我们得到如下结果:
`(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))))
现在,如果我们使用macroexpand 检查我们的进度:
(macroexpand '(safe2 [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 user/e
(clojure.core/str Error: (.getMessage user/e))))
您可能会注意到,在 Clojure 中,编译宏时会解析准引号符号。无法解析的符号使用当前命名空间限定(在本例中为user)。这样做的理由是它可以帮助您编写“卫生”的宏。但是,在这种情况下,我们不想解析 e 符号,因为无法为局部变量指定限定名称。
我们现在有几个选择。首先是基本放弃卫生。这在这种情况下有效,因为您没有在 catch 块内扩展任何用户提供的代码。因此,名称e 不会与用户变量发生冲突。此解决方案如下所示:
`(try
(let ~arg1 ~arg2)
(catch Exception ~'e (str "Error: " (.getMessage ~'e))))
注意使用~'e 而不仅仅是e。 ~是为了脱离准引号,那么我们用正则引号来引用e。它看起来有点奇怪,但它确实有效。
虽然上述解决方案有效,但可以说使用生成的符号而不是 e 会更好。这样,如果您更改宏以接受用户为catch 块提供的代码,您可以确定它仍然可以工作。在这种特殊情况下,“自动生成”符号完全符合要求。如下所示:
`(try
(let ~arg1 ~arg2)
(catch Exception e# (str "Error: " (.getMessage e#))))
基本上,每当 Clojure 阅读器在准引号形式中遇到带有尾随 # 的符号时,它将生成一个新的 gensym'd 符号并替换该符号的每个出现(即 e#)与gensym'd 之一。如果我们macroexpand 这个,我们会得到类似的东西:
(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__))))
如您所见,e# 的每次出现都被替换为机器生成的符号。这里e__66__auto__ 是自动生成的符号。
最后,虽然自动生成很方便,但它并不总是足够的。主要问题是,由于自动生成的符号是在 read 时生成的,因此准引号形式的每次 求值(即,展开 em> 宏)将使用相同的自动生成符号。在这种特殊情况下,这很好。但是,在某些情况下,如果使用嵌套宏形式,这可能会导致冲突。在这些情况下,每次扩展宏时都需要显式使用gensym'd 符号。使用这种方法,您的宏的主体将如下所示:
(let [e (gensym)]
`(try
(let ~arg1 ~arg2)
(catch Exception ~e (str "Error: " (.getMessage ~e)))))
这里e 是宏中的一个局部变量,其值是一个新符号(通过gensym)。在准引用中,我们必须转义 e 才能使用 gensym'd 值。
如果我们扩展它,我们会得到类似的东西:
(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))))
如果我们再次展开它,我们会发现G__771 被替换为不同的符号(可能是 G__774)。相比之下,自动生成的解决方案 (e#) 将始终对每次扩展使用相同的符号(至少在我们重新编译宏之前)。
希望这能让您更好地了解宏、符号和卫生。如果有任何不清楚的地方,请告诉我。