`
songry
  • 浏览: 83410 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

Clojure-JVM上的函数式编程语言(9)宏 作者: R. Mark Volkmann

阅读更多

 原帖地址: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,实际上是一个函数

对象而不是宏的名字被传递。

 

    对宏的调用是在读取时被处理的。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics