Макросы расширены во время компиляции. Им не нужен код eval
; скорее, они собирают код, который позже будет оцениваться во время выполнения. Другими словами, если вы хотите удостовериться, что код, переданный макросу, оценивается во время выполнения, а не во время компиляции, это говорит вам, что вы абсолютно должны неeval
его в определении макроса.
Название catch-compiler-error
является немного неправильным с учетом этого; если код, вызывающий ваш макрос, имеет ошибку компилятора (возможно, отсутствующую скобку), на самом деле нет ничего, что мог бы сделать ваш макрос, чтобы поймать его. Вы можете написать catch-runtime-error
макрос так:
(defmacro catch-runtime-error
[& body]
`(try
[email protected]
(catch Exception e#
e#)))
Вот как этот макрос работает:
- Возьмите в произвольное число аргументов и хранить их в последовательности, называемой
body
.
- Создать список с этими элементами:
- Символ
try
- Все выражения, передаваемые в качестве аргументов
- Другой список с этими элементами:
- Символ
catch
- Символ
java.lang.Exception
(квалифицированная версия Exception
)
- Уникальный новый символ, который мы можем сослаться позже как
e#
- тот же символ, который мы создали ранее
Это немного больше, чтобы проглотить все сразу. Давайте посмотрим на то, что он делает с некоторым фактическим кодом:
(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 вам определенно не следует использовать его, если вы не полностью понимаете проблемы, которые он поднимает в отношении сферы действия и контекста, в дополнение к другим проблемам, обсуждаемым в этом вопросе.
Спасибо! Это работает для моего предполагаемого приложения, однако я заметил, что он не поймает ошибку, если я передам метод, вызывая неправильное количество аргументов. Вместо этого он выдает исключение незаконного аргумента. Любые идеи почему? – MONODA43
@ MONODA43 Вы не можете поймать эту ошибку, потому что она выбрасывается во время компиляции. Единственными ошибками, которые вы можете поймать в Clojure с помощью 'try'-'catch', как это, являются ошибки времени выполнения. –
Это имеет смысл сейчас. Я хочу сделать это, потому что я пытаюсь создать инструмент, который программно определяет, какие протоколы реализуются с помощью реализаций матрицы core.matrix. В основном для оценки полноты реализации. – MONODA43