我可以不使用eval写这个宏吗? [英] Can I write this macro without using eval?

查看:111
本文介绍了我可以不使用eval写这个宏吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



到目前为止,我有:



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



但是,当然,我被告知, 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. 符号 try

    2. 以参数形式传递的所有表达式

    3. 包含以下元素的另一个列表:


      1. 符号 catch

      2. 符号 java.lang.Exception c> Exception )

      3. 一个独特的新符号,我们以后可以称为 e# li>
      4. 我们之前创建的相同符号


  3. ol>

    这是一个有点多吞下一次。让我们看看它对一些实际代码的作用:

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


    b $ b

    你可以看到,我不是简单地评估一个表单与你的宏作为它的第一个元素;这将扩展宏评估结果。我只是想做扩展步骤,所以我使用 macroexpand ,它给我这个:

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

    这的确是我们所期望的:一个包含符号尝试,我们的身体表达式,以及另一个列表与符号 catch java.lang.Exception 后跟两个唯一符号的副本。



    您可以通过直接评估它来检查这个宏是否符合您的要求:

     (catch-runtime-error(/ 4 2)(/ 1 0))
    ; => ; #error {
    ; :cause由零除
    ; :via
    ; [{:type java.lang.ArithmeticException
    ; :message由零除
    ; :at [clojure.lang.Numbers divideNumbers.java158]}]
    ; :trace
    ; [[clojure.lang.Numbers divideNumbers.java158]
    ; [clojure.lang.Numbers divideNumbers.java3808]
    ; ,,,}}

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

     (defprotocol Foo 
    (foo [this] ))

    (defprotocol Bar
    (bar [this]))

    (defrecord Baz []
    Foo
    (foo [ ]:

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

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

    但是,如上所述,错误使用类似这样的宏。您可以编写一个宏,该宏返回一段代码,该代码会在传入的代码中调用 eval ,从而将编译时间推回runtime:

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

    让我们测试一下宏展开,以确保它能正常工作:

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



    这扩展为:

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

    不过(我想说得很清楚) >不要这样做。如果你发现自己将编译时间推回到运行时,只是试图捕获这些错误,你几乎肯定做错了。



    我猜你已经看过这个问题,解释了 eval 很好。在Clojure中,你绝对不应该使用它,除非你完全理解它引起的问题范围和上下文,除了问题中讨论的其他问题。


    I'm trying to write a macro which will catch a compile time error in Clojure. Specifically, I would like to catch exceptions thrown when a protocol method, which has not been implemented for that datatype, is called and clojure.lang.Compiler$CompilerException is thrown.

    So far I have:

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

    But of course, I've been told that eval is evil and that you don't typically need to use it. Is there a way to implement this without using eval?

    I'm inclined to believe that eval is appropriate here since I specifically want the code to be evaluated at runtime and not at compile time.

    解决方案

    Macros are expanded at compile time. They don't need to eval code; rather, they assemble the code that will be later be evaluated at runtime. In other words, if you want to make sure that the code passed to a macro is evaluated at runtime and not at compile time, that tells you that you absolutely should not eval it in the macro definition.

    The name catch-compiler-error is a bit of a misnomer with that in mind; if the code that calls your macro has a compiler error (a missing parenthesis, perhaps), there's not really anything your macro can do to catch it. You could write a catch-runtime-error macro like this:

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

    Here's how this macro works:

    1. Take in an arbitrary number of arguments and store them in a sequence called body.
    2. Create a list with these elements:

      1. The symbol try
      2. All the expressions passed in as arguments
      3. Another list with these elements:

        1. The symbol catch
        2. The symbol java.lang.Exception (the qualified version of Exception)
        3. A unique new symbol, which we can refer to later as e#
        4. That same symbol that we created earlier

    This is a bit much to swallow all at once. Let's take a look at what it does with some actual code:

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

    As you can see, I'm not simply evaluating a form with your macro as its first element; that would both expand the macro and evaluate the result. I just want to do the expansion step, so I'm using macroexpand, which gives me this:

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

    This is indeed what we expected: a list containing the symbol try, our body expressions, and another list with the symbols catch and java.lang.Exception followed by two copies of a unique symbol.

    You can check that this macro does what you want it to do by directly evaluating it:

    (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]
    ;     ,,,]}
    

    Excellent. Let's try it with some protocols:

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

    However, as noted above, you simply can't catch a compiler error using a macro like this. You could write a macro that returns a chunk of code that will call eval on the rest of the code passed in, thus pushing compile time back to runtime:

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

    Let's test the macroexpansion to make sure this works properly:

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

    This expands to:

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

    Now we can catch even more errors, like IllegalArgumentExceptions caused by trying to pass an incorrect number of arguments:

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

    However (and I want to make this very clear), don't do this. If you find yourself pushing compile time back to runtime just to try to catch these sorts of errors, you're almost certainly doing something wrong. You'd be much better off restructuring your project so that you don't have to do this.

    I'm guessing you've already seen this question, which explains some of the pitfalls of eval pretty well. In Clojure specifically, you definitely shouldn't use it unless you completely understand the issues it raises regarding scope and context, in addition to the other problems discussed in that question.

    这篇关于我可以不使用eval写这个宏吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆