Выполнение макросов с именованными аргументами через Clojure Spec

Допустим, у нас есть макрос, который принимает один обязательный аргумент, за которым следуют необязательные позиционные аргументы, такие как

(require '[clojure.spec     :as spec]
         '[clojure.spec.gen :as gen])

(defmacro dress [what & clothes]
  `(clojure.string/join " " '(~what ~@clothes)))

(dress "me")
=> "me"
(dress "me" :hat "favourite")
=> "me :hat favourite"

и мы пишем для него спецификацию, например

(spec/def ::hat string?)
(spec/fdef dress
           :args (spec/cat :what string?
                           :clothes (spec/keys* :opt-un [::hat]))
           :ret string?)

мы обнаружим, что spec/exercise-fn не выполняет макрос

(spec/exercise-fn `dress)
;1. Unhandled clojure.lang.ArityException
;   Wrong number of args (1) passed to: project/dress

хотя данные, сгенерированные генератором функций, прекрасно принимаются макросом:

(def args (gen/generate (spec/gen (spec/cat :what string?
                                            :clothes (spec/keys* :opt-un [::hat])))))
; args => ("mO792pj0x")
(eval `(dress ~@args))
=> "mO792pj0x"
(dress "mO792pj0x")
=> "mO792pj0x"

С другой стороны, определение функции и ее выполнение одинаково хорошо работает:

(defn dress [what & clothes]
  (clojure.string/join " " (conj clothes what)))

(spec/def ::hat string?)
(spec/fdef dress
           :args (spec/cat :what string?
                           :clothes (spec/keys* :opt-un [::hat]))
           :ret string?)
(dress "me")
=> "me"
(dress "me" :hat "favourite")
=> "me :hat favourite"
(spec/exercise-fn `dress)
=> ([("") ""] [("l" :hat "z") "l :hat z"] [("") ""] [("h") "h"] [("" :hat "") " :hat "] [("m") "m"] [("8ja" :hat "N5M754") "8ja :hat N5M754"] [("2vsH8" :hat "Z") "2vsH8 :hat Z"] [("" :hat "TL") " :hat TL"] [("q4gSi1") "q4gSi1"])

И если мы посмотрим на встроенные макросы с похожими шаблонами определения, мы увидим ту же проблему:

(spec/exercise-fn `let)
; 1. Unhandled clojure.lang.ArityException
;    Wrong number of args (1) passed to: core/let

Интересно то, что exercise-fn отлично работает, когда всегда присутствует один обязательный именованный аргумент:

(defmacro dress [what & clothes]
  `(clojure.string/join " " '(~what ~@clothes)))

(spec/def ::hat string?)
(spec/def ::tie string?)
(spec/fdef dress
           :args (spec/cat :what string?
                           :clothes (spec/keys* :opt-un [::hat] :req-un [::tie]))
           :ret string?)
(dress "me" :tie "blue" :hat "favourite")
=> "me :tie blue :hat favourite"
(spec/exercise-fn `dress)

Другими словами: кажется, что при обычном вызове макросам всегда передаются некоторые скрытые аргументы, которые не передаются спецификацией. К сожалению, у меня недостаточно опыта работы с Clojure, чтобы знать о таких деталях, но маленькая птичка сказала мне, что есть вещи с именами & env и & form.

Но мой вопрос сводится к следующему: можно ли задать макрос с именованными аргументами таким образом, чтобы spec/exercise-fn мог дать ему хорошую тренировку?

Дополнение:

Заключение keys* в and, кажется, снова нарушает exercise-fn, даже если у него есть обязательный именованный arg.


person Rovanion    schedule 11.04.2017    source источник


Ответы (1)


Вы не можете использовать exercise-fn с макросами, как вы не можете использовать apply с макросами. (Обратите внимание, что это называется упражнением fn :).

Это в точности похоже на (apply dress ["foo"]), что дает знакомое "не может принимать значение макроса". Другое сообщение об ошибке, которое вы видите, связано с тем, что оно применяется к переменной, а не к макросу, поскольку то, что на самом деле происходит, похоже на (apply #'user/dress ["foo"]).

person Alex Miller    schedule 21.04.2017
comment
Ах. Итак, как мы используем наши макросы? clojure.spec.test/check путь? - person Rovanion; 21.04.2017
comment
Имейте в виду, что макрос - это (просто) функция, которая принимает код и возвращает код. Обычно не имеет смысла выполнять макрос так же, как выполнять функцию, потому что его вызов просто дает вам больше материала для компиляции. Я полагаю, вы можете использовать генератор: args для генерации входных данных, а затем выполнить вызов с этими входными данными. check фильтрует макросы, поэтому также не будет работать с макросами fdefs. - person Alex Miller; 22.04.2017