clojure macros lisp eval

clojure - ¿Puedo escribir esta macro sin usar eval?



macros lisp (1)

Las macros se expanden en tiempo de compilación. No necesitan eval código; más bien, ensamblan el código que luego será evaluado en tiempo de ejecución. En otras palabras, si quiere asegurarse de que el código pasado a una macro se evalúa en tiempo de ejecución y no en tiempo de compilación, eso le indica que no debería eval en la definición de la macro.

El nombre catch-compiler-error es un poco inapropiado con eso en mente; si el código que llama a su macro tiene un error de compilación (un paréntesis que falta, quizás), realmente no hay nada que su macro pueda hacer para atraparlo. Podría escribir una macro catch-runtime-error como esta:

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

Así es como funciona esta macro:

  1. Tome una cantidad arbitraria de argumentos y guárdelos en una secuencia llamada body .
  2. Crea una lista con estos elementos:
    1. El símbolo try
    2. Todas las expresiones pasadas como argumentos
    3. Otra lista con estos elementos:
      1. El símbolo catch
      2. El símbolo java.lang.Exception (la versión calificada de Exception )
      3. Un nuevo símbolo único, al que podemos referirnos más tarde como e#
      4. Ese mismo símbolo que creamos antes

Esto es demasiado para tragar todo a la vez. Echemos un vistazo a lo que hace con un código real:

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

Como puede ver, no estoy simplemente evaluando un formulario con su macro como primer elemento; eso expandiría la macro y evaluaría el resultado. Solo quiero hacer el paso de expansión, entonces estoy usando macroexpand , lo que me da esto:

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

Esto es de hecho lo que esperábamos: una lista que contiene el símbolo try , nuestras expresiones corporales y otra lista con los símbolos catch y java.lang.Exception seguidos de dos copias de un símbolo único.

Puede verificar que esta macro haga lo que quiere haciendo al evaluarla directamente:

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

Excelente. Probémoslo con algunos protocolos:

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

Sin embargo, como se señaló anteriormente, simplemente no puede detectar un error de compilación utilizando una macro como esta. Podría escribir una macro que devuelva un fragmento de código que llamará a eval en el resto del código que se transmite, lo que hará que el tiempo de compilación vuelva al tiempo de ejecución:

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

Probemos la macroexpansión para asegurarnos de que esto funcione correctamente:

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

Esto se expande a:

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

Ahora podemos detectar incluso más errores, como IllegalArgumentException , causados ​​por intentar pasar una cantidad incorrecta de argumentos:

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

Sin embargo (y quiero dejar esto muy claro), no hagas esto. Si te encuentras empujando el tiempo de compilación nuevamente al tiempo de ejecución solo para tratar de detectar este tipo de errores, seguramente estás haciendo algo mal. Sería mucho mejor reestructurar su proyecto para que no tenga que hacer esto.

Supongo que ya has visto esta pregunta , lo que explica bastante bien algunas de las trampas de eval . En Clojure específicamente, definitivamente no debería usarlo a menos que comprenda completamente los problemas que plantea con respecto al alcance y contexto, además de los otros problemas discutidos en esa pregunta.

Estoy intentando escribir una macro que capturará un error de tiempo de compilación en Clojure. Específicamente, me gustaría detectar excepciones lanzadas cuando se llama a un método de protocolo, que no se ha implementado para ese tipo de datos, y se clojure.lang.Compiler$CompilerException .

Hasta ahora tengo:

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

Pero, por supuesto, me han dicho que eval es malo y que normalmente no necesitas usarlo. ¿Hay alguna manera de implementar esto sin usar eval ?

Me inclino a creer que eval es apropiado aquí ya que específicamente quiero que el código sea evaluado en tiempo de ejecución y no en tiempo de compilación.