目录
当前位置: 首页 > 文档资料 > Clojure 入门教程 >

21 宏

优质
小牛编辑
140浏览
2023-12-01

宏是用来给语言添加新的结构,新的元素的。它们是一些在读入期(而不是编译期)就会实际代码替换的一个机制。

对于函数来说,它们的所有的参数都会被evaluate的, 而宏则会自动判断哪些参数需要evaluate。 这对于实现像 (if _condition_ _then-expr_ _else-expr_) 这样的结构是非常重要的。 如果 condition 是 true , 那么只有 "then" 表达式需要被evaluated. 如果条件是 false , 那么只有 "else" 表达式应该被 evaluated. 这意味着 if 不能被实现成一个函数 (它其实也不是宏, 而是一个special form)。其它一些因为这个原因而必须要实现成宏的包括 andor 因为它们需要实现 "short-circuit"属性。

要想知道一个东西到底是函数还是宏, 可以在REPL里面输入 (doc _name_) 或者查看它的元数据。如果是一个宏的话,那么它的元数据里面包含一个 :macro key, 并且它的值为 true 。 比如,我们要看看 and , 是不是宏, 在REPL里面输入下面的命令:

((meta (var and)) :macro) ; long way -> true
(^#'and :macro) ; short way -> true

让我们通过一些例子来看看如何编写并且使用宏。假设我们代码里面很多地方要对一个数字进行判断,通过判断它是接近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)))

Clojure的reader会把所有调用around-aero的地方全部换成defmacro这个方法体里面的具体代码。我们在这里使用let是为了性能,因为这个传进来的number是一个表达式而不是一个简单的值, 而且被cond语句里面使用了两次。自动产生的变量number#是为了产生一个不会和用户指定的其它binding冲突的一个名字。这使得我们可以创建 hygienic macros .

宏定义开始的时候的那个反引号 (也称为语法引号) 防止宏体内的任何一个表达式被evaluate -- 除非你显示地转义了。这意味着宏体里面的代码会原封不动地替换到使用这个宏的所有的地方 -- 除了以波浪号开始的那些表达式。 ( number , zero-expr , positive-exprnegative-expr ). 当一个名字前面被加了一个波浪号,并且还在反引号里面,它的值会被替换的。如果这个名字代表的是一个序列,那么我们可以用 [email protected] 这个语法来替换序列里面的某个具体元素。

下面是两个使用这个宏的例子:(输出都应该是 " + ").

(around-zero 0.1 (println "-") (println "0") (println "+"))
(println (around-zero 0.1 "-" "0" "+")) ; same thing

如果对于每种条件执行多于一个表达式, 那么用do把他们包起来。看下面例子:

(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 "-")))

下面是一个使用这个宏来返回一个描述输入数字的属性的字符串的函数。

(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

因为宏不会 evaluate 它们的参数, 所以你可以在宏体里面写一个对函数的参数调用. 函数定义不能这么做,相反只能用匿名函数把它们包起来。

下面是一个接受两个参数的宏。第一个是一个接受一个参数的函数, 这个参数是一个弧度, 如果它是一个三角函数sin, cos。第二个参数是一个弧度。如果这个被写成一个函数而不是一个 宏的话, 那么我们需要传递一个 #(Math/sin %) 而不是简单的 Math/sin 作为参数。注意 那些后面的#符号, 它会产生一个唯一的、不冲突的本地binding。 #~ 都必须在反引号引着的列表里面才能使用。

(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 , 传递的是函数而不是宏。

宏的调用是在读入期处理的。