跳转到内容

学习 Clojure/宏

来自维基教科书,开放的书籍,开放的世界

宏是函数,它们有效地允许我们创建自己的语法便利。对于许多 Lisp 程序员来说,宏是使 Lisp 成为 Lisp 的基本特性。

简单的宏

[编辑 | 编辑源代码]

如果你理解了 reader 和 evaluator,实际上关于宏的操作和创建就不需要再理解更多了,因为宏仅仅是一个以特殊方式调用的普通函数。当作为宏调用时,函数接收其参数求值,并返回一个表达式,该表达式将替换对宏的调用。一个非常简单(且无用)的宏将是一个简单地返回其参数的宏

(def pointless (fn [n] n))

传递给此宏的任何内容——列表、符号,任何内容——都将被原样返回,然后在调用后求值。实际上,调用此宏毫无意义

(pointless (+ 3 5))   ; pointless returns the list (+ 3 5), which is then evaluated in its place
(+ 3 5)               ; may as well just write this instead

但正如我们上面定义的无用,它只是一个普通函数,而不是宏。为了使其成为宏,我们需要将键值对:macro true作为元数据附加到由def映射到无用的 Var。有许多方法可以做到这一点,但最常见的做法是使用提供的宏clojure/defmacro简单地定义函数作为宏

(defmacro pointless [n] n)   ; define a macro pointless that takes one parameter and simply returns it

执行某些操作的宏

[编辑 | 编辑源代码]

一个真正有用的宏通常会返回一个语法引用的列表表达式。例如,考虑当 DEBUG 标志打开时执行某些操作,而标志关闭时不执行任何操作的情况。首先定义标志

(def DEBUG true)

现在,我们希望在它为真时执行某些操作,而在它们不为真时不执行。如果我们定义一个函数

(defn on-debug-fn [& args]
  (when DEBUG
    (eval `(do ~@args))))    ; Done this way to expand the list of args.

那么 (on-debug-fn "Debug") 确实仅在 DEBUG 为真时返回 "Debug"。但是,(on-debug-fn (println "Debug")) 始终打印 "Debug"。这是因为,对于函数来说,参数总是被求值的,并且它带有一个副作用:打印 "Debug"。

相反,我们需要的是一个宏。然后,就可以在不求值传递给它的形式的情况下检查 DEBUG 标志。

(defmacro on-debug [& body]
  `(when DEBUG
     (do ~@body)))

所以,让我们看看它做了什么

(macroexpand-1 '(on-debug (println "Debug"))) => (clojure.core/when user/DEBUG (do (println "Debug")))

(macroexpand-1 ...) 是一个函数,它显示给定宏扩展成什么。因此,它检查 user/DEBUG 的值,并且仅在 debug 不为 false 时才求值主体。仔细观察

(macroexpand
  '(on-debug
     (println "Debug"))) => (if* user/DEBUG
                               (do
                                 (do
                                   (println "Debug"))))

(macroexpand ...) 是一个函数,它扩展传递给它的形式中的所有宏。这表明 (when ...) 实际上是一个宏,它扩展成使用 (if* ...) 和 (do ...) 块进行检查,从而巧妙地使我们的 (do ...) 块变得多余。

所以宏的最终版本是

(defmacro on-debug [& body]
  `(when DEBUG
    ~@body))

现在,如果我们想要多个调试级别,其中 1 是基本信息,3 是痛苦的垃圾信息呢?

(defmacro on-debug-level [level & body]
  `(when (and DEBUG
           (<= ~level DEBUG))
      ~@body))

level 前缀有一个 unquote 符号,因此它不会被视为属于命名空间。DEBUG 没有前缀 unquote 符号,因为我们希望能够更改调试级别,而无需重新评估包含 (on-debug-level ...) 表单的每个函数。

相反,如果我们在宏中使用了 ~DEBUG,那么当使用 (on-debug-level ...) 的宏被求值时,~DEBUG 将被替换为 user/DEBUG 的值。这意味着,如果调试级别后来被更改,则所有这些宏也必须被重新评估,以便将新值放入条件中。

在 REPL 中测试,(on-debug-level 2 (println "Debug")) 确实在调试级别为 2 时打印 "Debug",而在调试级别为 1 时什么也不做。当然,这可以在函数中使用,以便有一个单一的方法来打印调试字符串。

(defn debug-println [level st]
   (on-debug-level level
                   (println st)))

宏作为控制结构

[编辑 | 编辑源代码]

此宏假设一个函数 (get-connection),它打开并返回与数据库的连接。它提供了一个典型的 Lisp with- 语法,其中它将 *conn* 绑定到连接,运行宏的主体,关闭连接并返回主体的返回值。

 (defonce *conn* nil)

这将绑定一个 var,*conn*,它将在宏的主体中使用以引用数据库连接。

(defmacro with-connection [& body]
  `(binding [*conn* (get-connection)]
     (let [ret# (do ~@body)]
       (.close *conn*)
       ret#)))

逐步执行此操作

(defmacro with-connection [& body] ...)

这使用可变参数语法来获取参数作为列表。

 `(binding ...

反引号运算符表示此表单中的所有内容都应该被引用,而不是求值(除非以 unquote 运算符 (~) 为前缀)。因此,binding 变成 clojure.core/binding,并且与之相关的所有内容都以列表的形式构建,而不是作为一系列函数调用。

 `(binding [*conn* (get-connection)]

这将 var *conn* 绑定起来,以便在此表单中,它具有 (get-connection) 返回的值。换句话说,在 (with-connection ...) 表单内的操作可以将 *conn* 视为数据库连接。

请注意,函数 (get-connection) 在宏实际使用之前不会被求值。这是因为反引号在 (binding ...) 之前。另一方面,如果我们希望在宏被定义时求值函数,则将使用 unquote (~) 运算符

`(binding [*conn* ~(get-connection)]

这将调用 (get-connection) 一次,在宏被定义时,所有对该宏的未来使用将使用 (get-connection) 最初返回的值。

    (let [ret# (do ~@body)]

此语句有点复杂。ret# 创建一个 gensymmed 名称;一个唯一的名称,确保如果符号 ret 在 (with-connection ...) 表单的主体中使用,它不会与宏定义中使用的符号冲突。

语句的后半部分解包主体并将其传递给 do。传递给宏的可变参数(如上面的 [& body])作为列表给出。~@(unquote-splicing)运算符用列表中包含的值替换列表。

如果没有 ~@ 运算符,语句将类似于以下内容

`(do (list 1 2 3)) => (do (clojure.core/list 1 2 3))

使用 ~@ 运算符,列表将被列表中的值替换,给出

`(do ~@(list 1 2 3)) => (do 1 2 3)

因此语句

    (let [ret# (do ~@body)]

将名称 ret# 绑定到通过求值主体返回的值。这样做是为了返回主体表单的值,而不是关闭数据库返回的值。

最后

      (.close *conn*)
      ret#)))

调用连接的 close 方法并返回主体的返回值。总而言之,这意味着

(with-connection
 (str *conn*))

打开数据库连接,将其绑定到 *conn*,捕获主体返回的值(在本例中,只是连接的字符串表示形式),关闭数据库连接并返回主体的返回值。

Previous page
构建 Jar 包
学习 Clojure Next page
并发编程
华夏公益教科书