原帖地址:http://java.ociweb.com/mark/clojure/article.html#Macros
作者:R. Mark Volkmann
译者:RoySong
宏(Macros)
宏被用来为语言添加新的功能结构。它们是在读取时(read-time)用来产生代码的代码。
函数总是要对它所有的参数求值,然而宏可以决定它的哪个参数被求值。这点对于实现诸如
(if condition then-expr else-expr )这样的form非常重要。如果
condition 为true,那么只有
"then"表达式会被求值。如果condition
为false,那么只有"else"表达式会被求值。这代表着if
不能被实现为函数(实际上它是一个特殊form,而不是一个宏)。其他同样因为这个原因被实
现为宏的form包括and和or,因为它们需要做“短循环”("short-circuit")。
为了确定一个指定的操作是作为函数还是宏来实现,既可以在REPL中输入(doc name ),也可以
检查它的元数据。如果是个宏的话,它的元数据会包含一个:macro关键字并有一个true值。比如,
为了确定and的实现类型,在REPL中输入以下内容:
((meta (var and)) :macro) ; long way -> true (^#'and :macro) ; short way -> true
让我们通过编写和使用宏来轻松地实现一些例子。假设我们的代码中有很多地方需要进行不同的
操作基于某个某个数字是否真正接近于0,正数或者负数。我们想避免代码重复。这必须采用宏而不
是函数来实现,因为在某个条件下应该是一条语句被求值而不是三条(正,负,0)。采用defmacro
宏来创建一个宏:
(defmacro around-zero [number negative-expr zero-expr positive-expr] `(let [number# ~number] ; so number is only evaluated once (cond (< (Math/abs number#) 1e-15) ~zero-expr (pos? number#) ~positive-expr true ~negative-expr)))
读取器会将对around-zero宏的调用展开到对
let
特殊form的调用上去。let特殊form里面包含了一个
对cond函数的调用,cond的参数就是各项条件以及对应的返回值。在这儿采用let特殊form是为了在
第一个参数number接收的是一个表达式而非简单值得情况下提升效率。它只会对number求值一次,
然后在cond里面两次采用这个值。而系统自动生成变量声明(auto-gensym)number#是用来产生
一个独特的符号名而不会和其他符号名冲突。这样就允许了对 hygienic macros 的创建。
宏定义开头的后引号(“`”,又名语法引证syntax quote,编者注:键盘上1左边那个
键,不是单引号)避免了里面的所有内容被求值,直到引文结束。这代表着宏主体里面的内容都会按照
字面被展开,除了带波浪线的元素外(在上面的例子中,是 number
, zero-expr
, positive-expr
和
negative-expr)。而这些在语法引证
列表中前面带波浪线的符号,都会在展开时以其对应的值来代替。
在语法引证列表中的绑定如果它的值是序列,则可以在它的前面加上~@来代替它的个体值。
下面是使用这个宏的两个例子,预期的输出都是“+”:
(around-zero 0.1 (println "-") (println "0") (println "+")) (println (around-zero 0.1 "-" "0" "+")) ; same thing
如果需要在某处进行不止一次的求值,则采用do特殊form来包装它们。举个例子,如果number代表
温度,而我们用一个log函数来将它写入到日志文件中,那么我们会这么编写:
(around-zero 0.1 (do (log "really cold!") (println "-")) (println "0") (println "+"))
为了验证这个宏是否正确地展开了,我们在REPL中输入以下内容:
(macroexpand-1 '(around-zero 0.1 (println "-") (println "0") (println "+")))
输入结果如下,不过实际中没有缩进:
(clojure.core/let [number__3382__auto__ 0.1] (clojure.core/cond (clojure.core/< (Math/abs number__3382__auto__) 1.0E-15) (println "0") (clojure.core/pos? number__3382__auto__) (println "+") true (println "-")))
下面的函数采用了around-zero宏,并将返回值封装成单词:
(defn number-category [number] (around-zero number "negative" "zero" "positive"))
下面是一些使用函数的例子:
(println (number-category -0.1)) ; -> negative (println (number-category 0)) ; -> zero (println (number-category 0.1)) ; -> positive
因为宏不会对其参数求值,所以未印证的函数名可以作为参数传递给宏,然后就可以构造对这些函数
的带参调用。函数定义无法做到这一点,与之替代的是传递一个匿名函数来包装对函数的调用。
下面有一个接收两个参数的宏,第一个参数是一个函数,它拥有一个参数用于接受一个弧度数值,就像
三角函数;第二个参数直接接收一个角度数值。如果在这儿采用函数定义来替代宏,我们就不得不采用
#(Math/sin %)这种形式来代替简单的
Math/sin。注意对
#号后缀的使用通过系统来生成独特的本地绑定
名,这通常是必要的来避免同其他绑定名冲突。#和~都只能在语法印证列表中使用。
(defmacro trig-y-category [fn degrees] `(let [radians# (Math/toRadians ~degrees) result# (~fn radians#)] (number-category result#)))
让我们实验一下,底下的调用预期的输出是 "zero", "positive", "zero"和"negative"。
(doseq [angle (range 0 360 90)] ; 0, 90, 180 and 270 (println (trig-y-category Math/sin angle)))
宏的名字不能作为参数传递给函数。举个例子,一个宏的名称and不能传递给函数reduce。一种变通方案
是定义一个匿名函数来调用宏。举个例子,采用(fn [x y] (and x y))
或者 #(and %1 %2)这样的形式。宏
会在读取时在匿名函数内部展开。当这个匿名函数作为参数传递给其他函数比如reduce,实际上是一个函数
对象而不是宏的名字被传递。
对宏的调用是在读取时被处理的。