Как проверить макрос 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 покажет мне фактическое против цели, и я могу визуально проверить.

Другие вопросы по тегам