Я бы хотел написать макрос Clojure с тегами-тегами
, который обертывает кучу форм и добавляет некоторые метаданные к имени каждой формы deftest
- в частности, добавить что-то к ключу :tags
, чтобы я мог поиграть с инструментом для запуска тестов с определенным тегом.
Одна из очевидных реализаций для с тегами-тегами
- это прохождение всего тела рекурсивно, модификация каждой формы deftest
так, как я нахожу. Но я недавно читал Let Over Lambda, и он делает хорошее замечание: вместо того, чтобы самому ходить по коду, просто оберните код в макролет
и позвольте компилятору ходить по нему за вас. Что-то вроде:
(defmacro with-test-tags [tags & body]
`(macrolet [(~'deftest [name# & more#]
`(~'~'deftest ~(vary-meta name# update-in [:tags] (fnil into []) ~tags)
~@more#))]
(do ~@body)))
(with-test-tags [:a :b]
(deftest x (...do tests...)))
Однако, есть очевидная проблема, что макрос deftest
продолжает рекурсивно расширяться навсегда. Вместо этого я мог бы заставить его расширяться до clojure.test/deftest
, избегая тем самым дальнейших рекурсивных расширений, но тогда я не смогу с пользой вложить экземпляры с тегами-тегами
для обозначения подгрупп тестов.
На данный момент, особенно для такого простого, как deftest
, похоже, что ходить по коду самому будет проще. Но интересно, знает ли кто-нибудь технику написания макроса, который "слегка модифицирует" определенные подвыражения, не повторяясь вечно.
Для любопытства: я рассматривал некоторые другие подходы, такие как наличие переменной переменной времени компиляции binding
able, которую я устанавливаю, когда иду вверх и вниз по коду, и использование этой переменной, когда, наконец, вижу deftest
, но так как каждый макрос возвращает только одно расширение, его привязок не будет на месте для следующего вызова макрорасширения.
Я только что сделал пост-ходовую реализацию, и пока она работает, она не уважает специальные формы, такие как quote
- она расширяется и внутри них.
(defmacro with-test-tags [tags & body]
(cons `do
(postwalk (fn [form]
(if (and (seq? form)
(symbol? (first form))
(= "deftest" (name (first form))))
(seq (update-in (vec form) [1]
vary-meta update-in [:tags] (fnil into []) tags))
form))
body)))
(Также, извините за возможный шум на общем теге - я подумал, что вы сможете помочь со странными макроштуками даже при минимальном опыте работы с Clojure.)