我可以不使用 eval 来编写这个宏吗?

Can I write this macro without using eval?

我正在尝试编写一个宏来捕获 Clojure 中的编译时错误。具体来说,我想捕获当调用尚未为该数据类型实现的协议方法并抛出 clojure.lang.Compiler$CompilerException 时抛出的异常。

到目前为止我有:

(defmacro catch-compiler-error [body] (try (eval body) (catch Exception e e)))

当然,有人告诉我 eval 是邪恶的,您通常不需要使用它。有没有不使用 eval 就可以实现的方法?

我倾向于认为 eval 在这里是合适的,因为我特别希望代码在运行时而不是在编译时求值。

宏在编译时展开。他们不需要 eval 编码;相反,它们 assemble 稍后将在运行时评估的代码。换句话说,如果你想确保传递给宏的代码是在运行时而不是编译时计算的,那就告诉你你绝对应该 而不是 eval它在宏定义中。

catch-compiler-error 这个名字有点用词不当;如果调用您的宏的代码有一个编译器错误(可能缺少括号),那么您的宏实际上无法执行任何操作来捕获它。你可以这样写一个 catch-runtime-error 宏:

(defmacro catch-runtime-error
  [& body]
  `(try
     ~@body
     (catch Exception e#
       e#)))

此宏的工作原理如下:

  1. 接受任意数量的参数并将它们存储在名为 body.
  2. 的序列中
  3. 创建包含以下元素的列表:
    1. 符号try
    2. 作为参数传入的所有表达式
    3. 包含这些元素的另一个列表:
      1. 符号catch
      2. 符号java.lang.ExceptionException的合格版本)
      3. 一个独特的新符号,我们稍后可以将其称为e#
      4. 我们之前创建的相同符号

一下子吞下这东西有点多。让我们看看它用一些实际代码做了什么:

(macroexpand
 '(catch-runtime-error
    (/ 4 2)
    (/ 1 0)))

如您所见,我不是简单地评估以您的宏作为第一个元素的表单;这将同时扩展宏 评估结果。我只想做扩展步骤,所以我使用 macroexpand,这给了我这个:

(try
  (/ 4 2)
  (/ 1 0)
  (catch java.lang.Exception e__19785__auto__
    e__19785__auto__))

这确实是我们所期望的:一个包含符号 try 的列表,我们的 body 表达式,以及另一个包含符号 catchjava.lang.Exception 的列表,后跟一个独特符号的两个副本。

您可以通过直接评估这个宏来检查它是否按照您的意愿执行操作:

(catch-runtime-error (/ 4 2) (/ 1 0))
;=> #error {
;    :cause "Divide by zero"
;    :via
;    [{:type java.lang.ArithmeticException
;      :message "Divide by zero"
;      :at [clojure.lang.Numbers divide "Numbers.java" 158]}]
;    :trace
;    [[clojure.lang.Numbers divide "Numbers.java" 158]
;     [clojure.lang.Numbers divide "Numbers.java" 3808]
;     ,,,]}

非常好。让我们尝试一些协议:

(defprotocol Foo
  (foo [this]))

(defprotocol Bar
  (bar [this]))

(defrecord Baz []
  Foo
  (foo [_] :qux))

(catch-runtime-error (foo (->Baz)))
;=> :qux

(catch-runtime-error (bar (->Baz)))
;=> #error {,,,}

但是,如上所述,您根本无法使用这样的宏捕获编译器错误。您 可以 编写一个宏 returns 一段代码,该代码块将调用 eval 传入的其余代码,从而将编译时间推回到运行时:

(defmacro catch-error
  [& body]
  `(try
     (eval '(do ~@body))
     (catch Exception e#
       e#)))

让我们测试宏展开以确保其正常工作:

(macroexpand
 '(catch-error
    (foo (->Baz))
    (foo (->Baz) nil)))

扩展为:

(try
  (clojure.core/eval
   '(do
      (foo (->Baz))
      (foo (->Baz) nil)))
  (catch java.lang.Exception e__20408__auto__
    e__20408__auto__))

现在我们可以捕捉到更多的错误,例如 IllegalArgumentException 尝试传递不正确数量的参数所导致的错误:

(catch-error (bar (->Baz)))
;=> #error {,,,}

(catch-error (foo (->Baz) nil))
;=> #error {,,,}

但是(我想说得很清楚),不要这样做。如果您发现自己将编译时间推回到运行时只是为了尝试捕捉这些类型的错误,你几乎肯定做错了什么。你最好重构你的项目,这样你就不必这样做了。

我猜您已经看过 this question,这很好地解释了 eval 的一些陷阱。特别是在 Clojure 中,除非您完全理解它提出的有关范围和上下文的问题,以及该问题中讨论的其他问题,否则您绝对不应该使用它。