学习 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*,捕获主体返回的值(在本例中,只是连接的字符串表示形式),关闭数据库连接并返回主体的返回值。