2016-03-14 3 views
3

Я пытаюсь написать макрос, который поймает ошибку времени компиляции в Clojure. В частности, я хотел бы поймать исключения, возникающие при вызове метода протокола, который не был реализован для этого типа данных, и вызывается clojure.lang.Compiler$CompilerException.Могу ли я написать этот макрос без использования eval?

До сих пор у меня есть:

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

Но, конечно же, мне сказали, что eval есть зло, и что вы обычно не должны использовать его. Есть ли способ реализовать это без использования eval?

Я склонен полагать, что eval уместен здесь, так как я специально хочу, чтобы код оценивался во время выполнения, а не во время компиляции.

ответ

10

Макросы расширены во время компиляции. Им не нужен код eval; скорее, они собирают код, который позже будет оцениваться во время выполнения. Другими словами, если вы хотите удостовериться, что код, переданный макросу, оценивается во время выполнения, а не во время компиляции, это говорит вам, что вы абсолютно должны неeval его в определении макроса.

Название catch-compiler-error является немного неправильным с учетом этого; если код, вызывающий ваш макрос, имеет ошибку компилятора (возможно, отсутствующую скобку), на самом деле нет ничего, что мог бы сделать ваш макрос, чтобы поймать его. Вы можете написать catch-runtime-error макрос так:

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

Вот как этот макрос работает:

  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))) 

Как вы можете видеть, я не просто оценить форму с макроса в качестве первого элемента; что оба будут расширять макрос и оценить результат.Я просто хочу, чтобы сделать шаг расширения, поэтому я использую macroexpand, что дает мне это:

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

Это действительно то, что мы ожидали: список, содержащий символ try, наши выражения тела, а другой список с символы catch и java.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 [email protected])) 
    (catch Exception e# 
     e#))) 

Давайте протестируем macroexpansion, чтобы убедиться, что это работает правильно:

(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__)) 

Теперь мы можем поймать даже больше ошибок, Li ка IllegalArgumentException ы вызваны пытается передать неверное число аргументов:

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

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

Однако (и я хочу сделать это очень ясно), не делает этого. Если вы заставите время компиляции вернуться во время выполнения, чтобы попытаться поймать подобные ошибки, вы почти наверняка делаете что-то неправильно. Вам будет намного лучше реструктурировать ваш проект, чтобы вам не пришлось это делать.

Я предполагаю, что вы уже видели this question, что объясняет некоторые подводные камни eval довольно хорошо. В Clojure вам определенно не следует использовать его, если вы не полностью понимаете проблемы, которые он поднимает в отношении сферы действия и контекста, в дополнение к другим проблемам, обсуждаемым в этом вопросе.

+0

Спасибо! Это работает для моего предполагаемого приложения, однако я заметил, что он не поймает ошибку, если я передам метод, вызывая неправильное количество аргументов. Вместо этого он выдает исключение незаконного аргумента. Любые идеи почему? – MONODA43

+0

@ MONODA43 Вы не можете поймать эту ошибку, потому что она выбрасывается во время компиляции. Единственными ошибками, которые вы можете поймать в Clojure с помощью 'try'-'catch', как это, являются ошибки времени выполнения. –

+0

Это имеет смысл сейчас. Я хочу сделать это, потому что я пытаюсь создать инструмент, который программно определяет, какие протоколы реализуются с помощью реализаций матрицы core.matrix. В основном для оценки полноты реализации. – MONODA43

Смежные вопросы