Как правильно использовать macroexpand-1
для тестирования макросов Clojure, когда вы не работаете в REPL?
Как я могу использовать macroexpand-1 из исходного файла вместо REPL
Ответы (2)
Проблема в том, что ваши выражения за пределами deftest
выполняются во время компиляции, в то время как *ns*
связаны, а внутри deftest
они выполняются позже, во время выполнения, где *ns*
не привязано.
Почему это важно? Потому что macroexpand
необходимо разрешить символ iiinc
в текущем пространстве имен, чтобы решить, макрос это или нет, и найти его определение, если это макрос, чтобы вызвать его. Итак, вы видите, что ваш macroexpand
работает во время компиляции, но не во время выполнения.
Каково решение? Конечно, не запускать тесты во время компиляции! Вместо этого вы должны правильно квалифицировать свои формы в пространстве имен, чтобы они не зависели от удобства времени компиляции *ns*
. Вы можете сделать это вручную, написав
(deftest t-stuff
(println "(macroexpand-1 '(my.ns/iiinc 2)) =>" (macroexpand-1 '(my.ns/iiinc 2)))))
Однако правильное решение состоит в том, чтобы делать то, что вы должны всегда делать при заключении в кавычки форм, предназначенных для последующей оценки, точно так же, как при написании макроса: использовать синтаксическую кавычку, а не обычную кавычку. Таким образом, компилятор определяет предполагаемое пространство имен для вас во время компиляции и вставляет его в форму, чтобы оно оставалось там во время выполнения:
(deftest t-stuff
(println "(macroexpand-1 `(iiinc 2)) =>" (macroexpand-1 `(iiinc 2)))))
Предположим, мы хотим протестировать макрос, добавляющий 3 к любому значению:
(defmacro iiinc [x]
`(+ 3 ~x))
Вместо того, чтобы работать в REPL, я обычно предпочитаю использовать свой любимый текстовый редактор/IDE для разработки кода и использовать плагин lein test-refresh
для непрерывного запуска моих модульных тестов. Однако это не работает при попытке использовать macroexpand-1
для итеративной разработки нового макроса.
Проблема, похоже, в некотором конфликте между macroexpand-1
и макросом deftest
. Таким образом, решение состоит в том, чтобы избегать использования macroexpand-1
внутри формы (deftest ...)
. Однако он отлично работает за пределами deftest
, даже если он все еще находится в исходном файле модульного теста. Вот пример:
; source file tst.clj.core
(newline)
(println "This works when not in (deftest ...)")
(println "(macroexpand-1 '(iiinc 2)) =>" (macroexpand-1 '(iiinc 2)))
(deftest t-stuff
(newline)
(println "But it is broken inside (deftest ...)")
(println "(macroexpand-1 '(iiinc 2)) =>" (macroexpand-1 '(iiinc 2)))
(newline)
(println "However, we can use the macro itself fine in our tests")
(println " (iiinc 2) =>" (iiinc 2))
(is (= 5 (iiinc 2)))) ; unit test works fine
Результаты вышеизложенного таковы:
This works when not in (deftest ...)
(macroexpand-1 '(iiinc 2)) => (clojure.core/+ 3 2)
Testing tst.clj.core
But it is broken inside (deftest ...)
(macroexpand-1 '(iiinc 2)) => (iiinc 2)
However, we can use the macro itself fine in our tests
(iiinc 2) => 5
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
Постскриптум
См. следующий ответ для более полного обсуждения того, как написать макрос в Clojure:
Как написать макрос многопоточности Clojure?