我可以在不使用eval的情况下编写此宏吗?

时间:2016-03-14 01:07:25

标签: clojure macros lisp eval

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

到目前为止,我有:

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

但当然,我被告知eval是邪恶的,而你通常不需要使用它。有没有办法在不使用eval的情况下实现这一点?

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

1 个答案:

答案 0 :(得分:10)

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

catch-compiler-error这个名字有点用词不当;如果调用你的宏的代码有一个编译器错误(可能是一个缺少的括号),那么你的宏可以做什么来捕获它。您可以像这样写一个catch-runtime-error宏:

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

以下是这个宏的工作原理:

  1. 接受任意数量的参数并将其存储在名为body的序列中。
  2. 使用以下元素创建列表:
    1. 符号try
    2. 作为参数传递的所有表达式
    3. 包含以下元素的另一个列表:
      1. 符号catch
      2. 符号java.lang.ExceptionException的合格版本)
      3. 一个独特的新符号,我们稍后可以将其称为e#
      4. 我们之前创建的那个符号
  3. 这有点可以一下子吞下去。让我们来看看它对一些实际代码的作用:

    (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的列表,我们的正文表达式,以及带有符号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 {,,,}
    

    但是,如上所述,您无法使用像这样的宏来捕获编译器错误。您可以编写一个宏,该宏返回一段代码,这些代码将在传入的其余代码上调用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中,除非你完全理解它在范围和背景方面提出的问题,除了该问题中讨论的其他问题之外,你绝对不应该使用它。