我是Clojure的新手,我无法理解其报价系统。我正在写一个宏,我做了两个相似的案例 - 一个是有效的,另一个则没有。从某种意义上说,我只是试图用try / catch条件包围我的语句。
以下是有效的代码:
(defmacro safe
[arg1 arg2]
(list 'let arg1 arg2)
)
这是不起作用的代码
(defmacro safe
[arg1 arg2]
'(try
~(list 'let arg1 arg2)
(catch Exception e (str "Error: " (.getMessage e)))
)
)
在~
符号之后,它应该转义引号,但由于某种原因,它似乎没有。错误是:“无法解析符号:此上下文中的arg1 ......”。
感谢您的帮助!
编辑:
我用以下代码调用宏的代码:
(println (safe [s (new FileReader (new File "text.txt"))] (.read s)))
另外,我导入了这个:
(import java.io.FileReader java.io.File)
目标是从文件中读取第一个符号,同时保护文件名不正确。顺便说一句,这是我的学校作业,所以我不应该使用任何其他方式来做这个,并且必须像这样调用宏,我知道with-open
等。
答案 0 :(得分:8)
转义(~
)仅适用于准引用(也称为语法引用)。您需要使用“返回引用”(`
,在与大多数美国键盘上的~
相同的键上找到),而不是正常的单引号('
,它是打开的与"
相同的密钥。这是图形上的细微差别,很容易错过。
您也可以通过不引用list
和不引用let
和arg1
来摆脱arg2
。通过这些更改,我们得到类似的结果:
`(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
}。如果我们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__
是自动生成的符号。
最后,虽然auto-gen很方便,但并不总是足够。主要问题是,因为自动生成的符号是在读时生成的,所以准报价形式的每个评估(即扩展 em> of macro)将使用相同的自动生成符号。在这个特殊情况下,那没关系。但是,在某些情况下,如果使用嵌套的宏窗体,则会导致冲突。在这些情况下,每次扩展宏时都必须使用明确的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#
)将始终对每个扩展使用相同的符号(至少在我们重新编译宏之前)。
希望这可以让您更好地了解宏,符号和卫生。如果有什么不清楚,请告诉我。
答案 1 :(得分:0)
这里有两个问题:
首先,unsplicing(〜和〜@)仅在syntax-quote(`)中起作用。通常为宏选择语法引用,因为它还在宏定义位置执行符号命名空间解析。简单引号(')将保持符号不变,因此ns解析将在宏调用站点发生。因为您无法控制调用宏的位置和方式,所以可能会非常混乱。
其次,您不能仅在引用代码中声明新符号,这可能会导致名称与宏周围的代码冲突。宏引入的每个新符号都应使用后缀#
,因此Clojure宏扩展将使用新的自动生成的名称替换它,这不会导致任何名称与用户代码冲突。
(defmacro m []
`(let [x# 1]
x#))
(macroexpand-1 '(m)) =>
=> (clojure.core/let [x__6257__auto__ 1]
x__6257__auto__)
注意let如何成为完全限定的clojure.core / let(以后避免ns解析的细微差别),x#被替换为x__6257__auto__(避免名称冲突)。
您的代码应该写成:
(defmacro safe [arg1 arg2]
`(try
(let ~arg1 ~arg2)
(catch Exception e#
(str "Error: " (.getMessage e#)))))
像这样检查:
(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__))))
我还建议使用宏args的惯用名并制作任意长度的第二个参数:
(defmacro safe-let [bindings & body]
`(try
(let ~bindings
~@body)
(catch Exception e#
(str "Error: " (.getMessage e#)))))