学习 Clojure/Reader 宏
Reader 宏(不要与普通宏混淆)是一个特殊的字符序列,当 Reader 遇到它时,会修改 Reader 的行为。Reader 宏的存在是为了语法上的简洁和方便。
'foo ; (quote foo)
#'foo ; (var foo)
@foo ; (clojure.core/deref foo)
#^{:ack bar} foo ; (clojure.core/with-meta foo {:ack bar}) ^{:ack bar} foo ; (clojure.core/with-meta foo {:ack bar})
#"regex pattern" ; create a java.util.regex.Pattern from the string (this is done at read time, ; so the evaluator is handed a Pattern, not a form that evaluates into a Pattern)
#(foo %2 bar %) ; (fn [a b] (foo b bar a))
#()
语法用于作为参数传递的非常短的函数。它接受名为 %
,%2
,%3
,%n
... %&
的参数。
最复杂的 Reader 宏是 语法引号,用 `(反引号)表示。当它用于符号时,语法引号类似于引号,但符号会解析为它的完全限定名。
`meow ; (quote cat/meow) ...assuming we are in the namespace cat
将语法引号应用于原子值会扩展为该值本身。例如
`10 ; expands to 10 `1/2 ; expands to 1/2 `"hello" ; expands to "hello"
当它用于列表、向量或映射形式时,语法引号会引用整个形式,除了 a)所有符号都会解析为它们的完全限定名,以及 b)以 ~ 开头的组件会被 取消引用
(defn rabbit [] 3) `(moose ~(rabbit)) ; (quote (cat/moose 3)) ...assume namespace cat
(def zebra [1 2 3]) `(moose ~zebra) ; (quote (cat/moose [1 2 3]))
以 ~@ 开头的组件会被 取消引用拼接
`(moose ~@zebra) ; (quote (cat/moose 1 2 3))
如果一个符号是非命名空间限定的并且以 '#' 结尾,它会被解析为一个具有相同名称的生成的符号,其中附加了 '_' 和唯一的 ID。例如 x# 会解析为 x_123。在语法引号表达式中对该符号的所有引用都会解析为相同的生成符号。
`(x#) ; (x__2804__auto__)
对于除符号、列表、向量和映射之外的所有形式,`x 与 'x 相同。
语法引号可以嵌套在其他语法引号中
`(moose ~(squirrel `(whale ~zebra)))
对于列表,语法引号建立了相应数据结构的模板。在模板中,非限定形式的行为就像递归语法引号一样。
`(x1 x2 x3 ... xn)
被解释为
(clojure.core/seq (clojure.core/concat |x1| |x2| |x3| ... |xn|))
其中 | | 用于指示对 xj 的转换,如下所示
- |form| 被解释为 (clojure.core/list `form),它包含一个语法引号形式,然后必须对其进行进一步解释。
- |~form| 被解释为 (clojure.core/list form)。
- |~@form| 被解释为 form。
如果语法引号语法嵌套,则最内部的语法引号形式会首先展开。这意味着,如果在一个行中出现多个 ~,则最左边的 ~ 属于最内部的语法引号。
一个重要的例外是空列表
`()
被解释为
(clojure.core/list)
根据以上规则,并假设 var a 包含 5,一个像这样的表达式
``(~~a)
会(在幕后)扩展为如下所示
(clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/seq)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/concat)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/list)) (clojure.core/list a)))))))))
然后计算,得到;
(clojure.core/seq (clojure.core/concat (clojure.core/list 5)))
当然,相同的表达式也可以等效地扩展为
(clojure.core/list `list a)
这实际上更容易阅读。Clojure 使用前一种算法,这种算法在也有拼接的情况下更普遍适用。
其原则是,具有嵌套深度为 k 的语法引号的表达式的结果,只有在执行了 k 次连续计算后才会相同,无论扩展算法如何(Guy Steele)。
对于向量、映射和集合,我们有以下规则
`[x1 x2 x3 ... xn] ; is interpreted as (clojure.core/apply clojure.core/vector `(x1 x2 x3 ... xn))
`{x1 x2 x3 ... xn} ; is interpreted as (clojure.core/apply clojure.core/hash-map `(x1 x2 x3 ... xn))
`#{x1 x2 x3 ... xn} ; is interpreted as (clojure.core/apply clojure.core/hash-set `(x1 x2 x3 ... xn))
如果我们定义语法引号表达式返回什么,则语法引号最容易理解。要计算语法引号表达式,您需要删除语法引号和每个匹配的波浪号,并将每个匹配波浪号之后的表达式替换为其值。计算以波浪号开头的表达式会导致错误。
如果在两个波浪号和语法引号之间有相同数量的波浪号和语法引号,则波浪号与语法引号匹配,其中 b 位于 a 和 c 之间,如果 a 附加到包含 b 的表达式,并且 b 附加到包含 c 的表达式。这意味着在格式良好的表达式中,最外面的语法引号与最里面的波浪号匹配。
假设 x 计算为 user/a,而 user/a 计算为 1;并且 y 计算为 user/b,而 user/b 计算为 2。
您可以从 REPL 中准备以下内容
user=> (def x `a) user=> (def y `b) user=> (def a 1) user=> (def b 2)
要计算表达式
``(w ~x ~~y )
我们删除第一个语法引号,并计算任何匹配波浪号之后的表达式。最右边的波浪号是唯一与第一个语法引号匹配的波浪号。如果我们删除它并用它的值替换它之前缀的表达式 y,我们会得到
`(w ~user/x ~user/b)
注意 x 如何被“解析”为 user/x。在 Clojure 中,任何非限定符号都会被解析!这与 Common Lisp 不同(更好)。
在这个后者的表达式中,两个波浪号都与语法引号匹配,所以如果我们要依次计算它,我们会得到
(w user/a 2)
波浪号 at(~@)的行为类似于波浪号,除了它之前缀的表达式必须同时出现在包含序列中并返回一个列表或序列。然后,返回序列的元素会被拼接回包含序列。所以
``( w ~x ~~@(list `a `b))
计算为
`(w ~user/x ~user/a ~user/b)
目前,Clojure 不允许您定义自己的 Reader 宏,但这可能在未来会改变。