Реалистичная спецификация Clojure для функции с именованными аргументами

Скажем, у нас есть функция clothe, которая требует один позиционный аргумент person в дополнение к ряду необязательных именованных аргументов :hat, :shirt и :pants.

(defn clothe [person & {:keys [hat shirt pants]}]
  (str "Clothing " person " with " hat shirt pants "."))
(clothe 'me :hat "top hat")
=> "Clothing me with top hat."

Мой текущий способ написания спецификации для этой функции:

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

(spec/def ::person symbol?)
(spec/def ::clothing
  (spec/alt :hat   (spec/cat :key #{:hat}   :value string?)
            :shirt (spec/cat :key #{:shirt} :value string?)
            :pants (spec/cat :key #{:pants} :value string?)))

(spec/fdef clothe
           :args (spec/cat :person  ::person
                           :clothes (spec/* ::clothing))
           :ret string?)

Проблема в том, что он позволяет использовать списки аргументов, такие как

(clothe 'me :hat "top hat" :hat "nice hat")
=> "Clothing me with nice hat."

что, хотя и разрешено самим языком, вероятно, является ошибкой, когда бы она ни совершалась. Но, возможно, еще хуже то, что это делает сгенерированные данные нереалистичными для того, как обычно вызывается функция:

(gen/generate (spec/gen (spec/cat :person  ::person
                                  :clothes (spec/* ::clothing))))
=> (_+_6+h/!-6Gg9!43*e :hat "m6vQmoR72CXc6R3GP2hcdB5a0"
    :hat "05G5884aBLc80s4AF5X9V84u4RW" :pants "3Q" :pants "a0v329r25f3k5oJ4UZJJQa5"
    :hat "C5h2HW34LG732ifPQDieH" :pants "4aeBas8uWx1eQWYpLRezBIR" :hat "C229mzw"
    :shirt "Hgw3EgUZKF7c7ya6q2fqW249GsB" :pants "byG23H2XyMTx0P7v5Ve9qBs"
    :shirt "5wPMjn1F2X84lU7X3CtfalPknQ5" :pants "0M5TBgHQ4lR489J55atm11F3"
    :shirt "FKn5vMjoIayO" :shirt "2N9xKcIbh66" :hat "K8xSFeydF" :hat "sQY4iUPF0Ef58198270DOf"
    :hat "gHGEqi58A4pH2s74t0" :pants "" :hat "D6RKWJJoFLCAaHId8AF4" :pants "exab2w5o88b"
    :hat "S7Ti2Cb1f7se7o86I1uE" :shirt "9g3K6q1" :hat "slKjK67608Y9w1sqV1Kxm"
    :hat "cFbVMaq8bfP22P8cD678s" :hat "f57" :hat "2W83oa0WVWM10y1U49265k2bJx"
    :hat "O6" :shirt "7BUJ824efBb81RL99zBrvH2HjziIT")

И что еще хуже, если у вас есть рекурсивное определение с spec/*, нет возможности ограничить количество потенциально рекурсивных вхождений, генерируемых при выполнении тестов кода.

Итак, у меня возникает вопрос: есть ли способ указать именованные аргументы для функции, ограничивающей количество вхождений на ключ до одного?


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


Ответы (1)


Если мы посмотрим, как макрос require описан в clojure.core.specs, мы увидим, что он использует (spec/keys* :opt-un []) для указания именованных аргументов в списке зависимостей, таких как :refer и :as в (ns (:require [a.b :as b :refer :all])).

(s/def ::or (s/map-of simple-symbol? any?))
(s/def ::as ::local-name)

(s/def ::prefix-list
  (s/spec
    (s/cat :prefix simple-symbol?
           :suffix (s/* (s/alt :lib simple-symbol? :prefix-list ::prefix-list))
           :refer (s/keys* :opt-un [::as ::refer]))))

(s/def ::ns-require
  (s/spec (s/cat :clause #{:require}
                 :libs (s/* (s/alt :lib simple-symbol?
                                   :prefix-list ::prefix-list
                               :flag #{:reload :reload-all :verbose})))))

В документации не упоминается, для чего предназначены :req-un и :opt-un, но, с другой стороны, в Руководстве по спецификации упоминается, что они предназначены для указания неполных ключей. Возвращаясь к определению нашей функции, мы могли бы записать ее так:

(spec/def ::clothing (spec/keys* :opt-un [::hat ::shirt ::pants]))
(spec/def ::hat   string?)
(spec/def ::shirt string?)
(spec/def ::pants string?)
(spec/fdef clothe
           :args (spec/cat :person  ::person
                           :clothes ::clothing)
           :ret string?)

К сожалению, это не помогает функции, принимающей несколько экземпляров одного и того же именованного аргумента.

(stest/instrument `clothe)
(clothe 'me :hat "top hat" :hat "nice hat")
=> "Clothing me with nice hat."

Хотя это означает, что генератор максимально создает один экземпляр одного и того же ключа, что помогает с рекурсивными спецификациями.

(gen/generate (spec/gen (spec/cat :person ::person
                                  :clothes ::clothing)))
=> (u_K_P6!!?4Ok!_I.-.d!2_.T-0.!+H+/At.7R8z*6?QB+921A
    :shirt "B4W86P637c6KAK1rv04O4FRn6S" :pants "3gdkiY" :hat "20o77")
person Rovanion    schedule 06.04.2017