Как проверить макрос clojure, который использует gensyms?
Я хочу проверить макрос, который использует gensyms. Например, если я хочу проверить это:
(defmacro m1
[x f]
`(let [x# ~x]
(~f x#)))
Я могу использовать макро-расширение...
(macroexpand-1 '(m1 2 inc))
...получить...
(clojure.core/let [x__3289__auto__ 2] (inc x__3289__auto__))
Это легко для человека, чтобы убедиться, что он прав.
Но как я могу проверить это на практике, чисто автоматизированным способом? Генсим не стабилен.
(Да, я знаю, что конкретный пример макроса не является убедительным, но вопрос все еще справедлив.)
Я понимаю, что выражения Clojure можно рассматривать как данные (это гомоиконический язык), поэтому я могу разбить результат следующим образом:
(let [result (macroexpand-1 '(m1 2 inc))]
(nth result 0) ; clojure.core/let
(nth result 1) ; [x__3289__auto__ 2]
((nth result 1) 0) ; x__3289__auto__
((nth result 1) 0) ; 2
(nth result 2) ; (inc x__3289__auto__)
(nth (nth result 2) 0) ; inc
(nth (nth result 2) 1) ; x__3289__auto__
)
Но это громоздко. Есть ли лучшие способы? Возможно, есть библиотеки для проверки структуры данных, которые могут пригодиться? Может быть, разрушение облегчит это? Логическое программирование?
ОБНОВЛЕНИЕ / КОММЕНТАРИЙ:
Хотя я ценю советы опытных людей, которые говорят: "Не проверяйте само макро-расширение", это не дает прямого ответа на мой вопрос.
Что плохого в "модульном тестировании" макроса путем тестирования макроподключения? Тестирование расширения является разумным - и на самом деле, многие люди проверяют свои макросы таким образом "вручную" в REPL - так почему бы не проверить это автоматически? Я не вижу веской причины не делать этого. Я признаю, что тестирование макро-расширения более хрупко, чем тестирование результата, но выполнение первого может иметь ценность. Вы также можете проверить функциональность - вы можете сделать оба! Это не решение.
Вот мое психологическое объяснение. Одна из причин, по которой люди не тестируют макро-расширение, состоит в том, что в настоящее время это немного болезненно. В целом, люди часто рационализируют себя против того, чтобы что-то делать, когда это кажется трудным, независимо от его внутренней ценности. Да, именно поэтому я задал этот вопрос! Если бы это было легко, я думаю, что люди делали бы это чаще. Если бы это было легко, они с меньшей вероятностью рационализировали бы, дав ответы, говорящие, что "это не стоит делать".
Я также понимаю аргумент, что "вы не должны писать сложный макрос". Конечно. Но давайте надеяться, что люди не зайдут так далеко, чтобы подумать: "Если мы будем поощрять культуру не тестировать макросы, то это помешает людям писать сложные". Такой аргумент был бы глупым. Если у вас есть сложное макро-расширение, то проверка его работоспособности, как вы ожидаете, - нормальная вещь. Я лично не испытываю даже простых вещей, потому что часто удивляюсь тому, что ошибки могут происходить из-за простых ошибок.
2 ответа
Не проверяйте, как это работает (его расширение), проверьте, что это работает. Если вы тестируете конкретное расширение, вы прикованы к этой стратегии реализации; вместо этого, просто проверьте это (m1 2 inc)
возвращает 3, и любые другие тестовые примеры необходимы для успокоения вашей совести, и тогда вы можете быть счастливы, что ваш макрос работает.
Это можно сделать с помощью метаданных. Ваш макрос выводит список, к которому могут быть прикреплены метаданные. Просто добавьте сопоставления gensym->var к этому, а затем используйте их для тестирования.
Итак, ваш макрос будет выглядеть примерно так:
(defmacro m1 [x f]
(let [xsym (gensym)]
(with-meta
`(let [~xsym ~x]
(~f ~xsym))
{:xsym xsym})))
Вывод из макроса теперь имеет карту против него с gensyms:
(meta (macroexpand-1 '(m1 a b)))
=> {:xsym G__24913}
Чтобы проверить расширение макроса, вы должны сделать что-то вроде этого:
(let [out (macroexpand-1 `(m1 a b))
xsym (:xsym (meta out))
target `(clojure.core/let [~xsym a] (b ~xsym))]
(= out target))
Чтобы ответить на вопрос, почему вы хотели бы сделать это: способ, которым я обычно пишу макрос, состоит в том, чтобы сначала сгенерировать целевой код (т. Е. То, что я хочу, чтобы макрос выводил), проверить, что все правильно, а затем сгенерировать макрос из этого, Наличие заранее известного хорошего кода позволяет мне использовать TDD против макроса; в частности, я могу настроить макрос, запустить тесты, и если они не пройдут clojure.test
покажет мне фактическое против цели, и я могу визуально проверить.