首页 > 解决方案 > Clojure Cond 宏的工作原理

问题描述

我对 Clojure 相当满意,但一直回避宏。为了解决这个问题,我正在通读“掌握 Clojure 宏”,并且还普遍地查看了一些 Clojure 核心宏。

在阅读cond宏时,我对实际评估的时间有点困惑。假设clauses不是 nil 并且初始when测试通过,然后我们评估列表调用。List 是一个函数,因此它必须首先评估它的所有参数,然后才能进入它的主体。第一个参数只是符号'if,下一个参数是然后(first claues)计算第一个测试,但是我发现有点令人困惑的部分是下一个(第三个)参数会发生什么。它看起来像整个表格:

(if (next clauses)
    (second clauses)
    (throw (IllegalArgumentException.
            "cond requires an even number of forms")))


实际上是在返回最终的宏展开进行评估之前进行评估。如果这是正确的,这是否意味着对偶数个表单的测试发生在宏实际展开之前,因此可以在宏实际生成一个运行时评估列表之前出现异常?

(defmacro cond
  "Takes a set of test/expr pairs. It evaluates each test one at a
  time.  If a test returns logical true, cond evaluates and returns
  the value of the corresponding expr and doesn't evaluate any of the
  other tests or exprs. (cond) returns nil."
  {:added "1.0"}
  [& clauses]
    (when clauses
      (list 'if (first clauses)
            (if (next clauses)
                (second clauses)
                (throw (IllegalArgumentException.
                         "cond requires an even number of forms")))
            (cons 'clojure.core/cond (next (next clauses))))))

标签: clojuremacros

解决方案


查看宏如何工作的最简单方法是使用clojure.core/macroexpand-1或检查它clojure.walk/macroexpand-all

例如,我们可以看到如何扩展以下表单:

(cond
  (pos? 1) :positive
  (neg? -1) :negative)

macroexpand-1

(macroexpand-1
  '(cond
    (pos? 1) :positive
    (neg? -1) :negative))

;; => (if (pos? 1) :positive (clojure.core/cond (neg? -1) :negative))

我们可以看到,当该表单展开时,绑定clauses到以下表达式的序列:(pos? 1):positive和。(neg? -1):negative

(first clauses)将评估为(pos? 1),其值将用作发出的测试表达式if。然后,宏通过检查第一个谓词是否有多个子句来检查它是否具有所需的结果表达式:(next clauses)将评估(:positive (neg? -1) :negative)哪个是真实的,发出的真正分支if将获得(second clauses)哪个是的值:positive

发出的 else 分支if将得到(clojure.core/cond (neg? -1) :negative). 由于发出的代码将再次包含对cond宏的调用,因此将再次调用它并再次展开。

要查看完全扩展的代码,我们可以使用clojure.walk/macroexpand-all

(require 'clojure.walk)

(clojure.walk/macroexpand-all
      '(cond
        (pos? 1) :positive
        (neg? -1) :negative))
;; => (if (pos? 1) :positive (if (neg? -1) :negative nil))

如果在宏扩展期间对包含的表单clauses进行评估,为了扩展主题,我们可以在代码中注入一些副作用:

(clojure.walk/macroexpand-all
  '(cond
     (do
       (println "(pos? 1) evaluated!")
       (pos? 1))
     (do
       (println ":positive evaluated1")
       :positive)

     (do
       (println "(neg? -1) evaluated!")
       (neg? -1))
     (do
       (println ":negative evaluated!")
       :negative)))
=>
(if
 (do (println "(pos? 1) evaluated!") (pos? 1))
 (do (println ":positive evaluated1") :positive)
 (if (do (println "(neg? -1) evaluated!") (neg? -1)) (do (println ":negative evaluated!") :negative) nil))

正如我们所见,没有执行任何副作用,因为在宏扩展期间没有评估任何子句。

我们还可以检查调用throw是否在宏扩展期间被评估,方法是提供clauses这将导致(if (next clauses) ...调用的 else 分支:

(macroexpand-1 '(cond (pos? 1)))
java.lang.IllegalArgumentException: cond requires an even number of forms

在这里我们可以看到异常被抛出并且宏的宏扩展cond没有通过返回宏扩展代码正常完成。在宏扩展期间计算表单的原因throw是它没有被引用(例如 ``(throw ...)`)。


推荐阅读