Я хотел бы написать макрос Clojure with-test-tags, который обертывает кучу форм и добавляет некоторые метаданные к имени каждой формы deftest - в частности, добавляет некоторые вещи в ключ :tags так что я могу играть с инструментом для запуска тестов с определенным тегом.
Одной очевидной реализацией для with-test-tags является рекурсивное перемещение всего тела, изменение каждой формы deftest, как я ее нахожу. Но я недавно читал Let Over Lambda, и он делает хороший вывод: вместо того, чтобы просто пропустить код, просто оберните код в macrolet и дайте компилятору пройти его для вас. Что-то вроде:
(defmacro with-test-tags [tags & body]
`(macrolet [(~'deftest [name# & more#]
`(~'~'deftest ~(vary-meta name# update-in [:tags] (fnil into []) ~tags)
[email protected]#))]
(do [email protected])))
(with-test-tags [:a :b]
(deftest x (...do tests...)))
Это имеет очевидную проблему, однако, что макрос deftest продолжает рекурсивно расширяться навсегда. Я мог бы расширить его до clojure.test/deftest, избегая при этом каких-либо дальнейших рекурсивных расширений, но тогда я не могу с пользой вставить экземпляры with-test-tags для подписи подгрупп тестов.
В этот момент, особенно для чего-то простого, как deftest, похоже, что ходьба кода сама по себе будет проще. Но мне интересно, знает ли кто-нибудь технику написания макроса, который "слегка изменяет" некоторые подвыражения, без повторения навсегда.
Для любопытных: я рассмотрел некоторые другие подходы, такие как наличие времени binding -able var, которое я установил при прохождении вверх и вниз по коду, и используя этот var, когда я наконец увижу deftest, но поскольку каждый макрос возвращает только одно расширение, его привязки не будут доступны для следующего вызова макроэкспонента.
Изменить
Я выполнил реализацию postwalk только сейчас, и, хотя он работает, он не соблюдает специальные формы, такие как 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)))
(Кроме того, извините за возможные помехи в теге common- lisp – я подумал, что вы можете помочь с более сложными макросами даже при минимальном опыте Clojure.)