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

标签 clojure macros lisp eval

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

到目前为止我有:

(defmacro catch-compiler-error [ body ] (尝试 (评估机构) (捕获异常 e e)))

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

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

最佳答案

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

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

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

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

  1. 接收任意数量的参数并将它们存储在名为 body 的序列中。
  2. 创建一个包含这些元素的列表:
    1. 符号尝试
    2. 所有作为参数传入的表达式
    3. 包含这些元素的另一个列表:
      1. 符号catch
      2. 符号java.lang.Exception(Exception的合格版本)
      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 的列表、我们的主体表达式,以及另一个包含符号 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]
;     ,,,]}

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

(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 {,,,}

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

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

关于clojure - 我可以不使用 eval 来编写这个宏吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35977973/

相关文章:

clojure - 将 Clojure 向量的内容提取为 LazySeq

powershell - 在 Powershell 中运行 Access 宏

android - 如何在Android Studio中扩展C++宏?

lisp - 有办法保存 Common Lisp 或 Scheme 的 REPL 状态吗?

.net - 确定远程设备端点 UDP Clojure CLR

postgresql - 如何使用 Clojure 将 PostgreSQL 函数应用于查询?

haskell - 每个请求数据的中间件

c++ - c/c++编译时 "compatibility"检查

lisp - 从其他列表中删除列表

lisp - Lisp 中的变量和符号是否不同?