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

MON*_*A43 3 lisp macros eval clojure

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

到目前为止,我有:

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

但当然,我被告知这eval是邪恶的,你通常不需要使用它.有没有办法实现这个而不使用eval

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

Sam*_*tep 10

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

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

(defmacro catch-runtime-error
  [& body]
  `(try
     ~@body
     (catch Exception e#
       e#)))
Run Code Online (Sandbox Code Playgroud)

以下是此宏的工作原理:

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

这有点可以一下子吞下去.让我们来看看它对一些实际代码的作用:

(macroexpand
 '(catch-runtime-error
    (/ 4 2)
    (/ 1 0)))
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,我不是简单地将您的宏作为其第一个元素来评估表单; 这将扩大宏评估结果.我只是想做扩展步骤,所以我正在使用macroexpand,这给了我:

(try
  (/ 4 2)
  (/ 1 0)
  (catch java.lang.Exception e__19785__auto__
    e__19785__auto__))
Run Code Online (Sandbox Code Playgroud)

这确实是我们所期望的:包含符号列表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]
;     ,,,]}
Run Code Online (Sandbox Code Playgroud)

优秀.让我们尝试一些协议:

(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 {,,,}
Run Code Online (Sandbox Code Playgroud)

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

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

让我们测试宏扩展以确保它正常工作:

(macroexpand
 '(catch-error
    (foo (->Baz))
    (foo (->Baz) nil)))
Run Code Online (Sandbox Code Playgroud)

这扩展到:

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

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

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

(catch-error (foo (->Baz) nil))
;=> #error {,,,}
Run Code Online (Sandbox Code Playgroud)

但是(我想说清楚),不要这样做.如果你发现自己将编译时间推回到运行时只是为了尝试捕获这些错误,那么你几乎肯定会做错事.重建项目要好得多,这样你就不用这么做了.

我猜你已经看过这个问题了,这个问题eval很好地解释了一些陷阱.特别是在Clojure中,除了完全理解它在范围和上下文中引起的问题之外,除了该问题中讨论的其他问题之外,你绝对不应该使用它.