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

Начнем с обычной последовательности

(require '[clojure.spec     :as spec]
         '[clojure.spec.gen :as gen])
(spec/def ::cat (spec/cat :sym symbol? :str string? :kws (spec/* keyword?)))

что соответствует векторам

(spec/conform ::cat '[af "5"])
=> {:sym af, :str "5"}
(spec/conform ::cat '[af "5" :key])
=> {:sym af, :str "5", :kws [:key]}

но и списки

(spec/conform ::cat '(af "5"))
=> {:sym af, :str "5"}
(spec/conform ::cat '(af "5" :key))
=> {:sym af, :str "5", :kws [:key]}

Если мы хотим ограничить это, мы можем попробовать использовать spec/tuple; но, к сожалению, он соответствует только векторам фиксированной длины, т.е. требует, чтобы как минимум пустой список был последней частью кортежа:

(spec/def ::tuple (spec/tuple symbol? string? (spec/* keyword?)))
(spec/conform ::tuple '[af "5"])
=> :clojure.spec/invalid
(spec/exercise ::tuple)
=> ([[r "" ()] [r "" []]] [[kE "" (:M)] [kE "" [:M]]] ...)

Мы также можем попробовать добавить дополнительное условие к ::cat с помощью spec/and:

(spec/def ::and-cat
  (spec/and vector? (spec/cat :sym symbol? :str string? :kws (spec/* keyword?))))

что соответствует отлично

(spec/conform ::and-cat '[af "5"])
=> {:sym af, :str "5"}
(spec/conform ::and-cat '[af "5" :key])
=> {:sym af, :str "5", :kws [:key]}
(spec/conform ::and-cat '(af "5" :key))
=> :clojure.spec/invalid

но, к сожалению, не может генерировать свои собственные данные, поскольку генератор для spec/cat создает только списки, которые, конечно, не будут соответствовать предикату vector?:

(spec/exercise ::and-cat)
=> Couldn't satisfy such-that predicate after 100 tries.

Итак, подведем итог: как написать спецификацию, которая способна одновременно принимать и генерировать векторы, такие как [hi "there"] [my "dear" :friend]?

Можно также перефразировать вопрос так: «Есть ли альтернатива spec/cat, которая генерирует векторы вместо списков?» или "Можно ли передать аргумент :kind в spec/cat?" или «Могу ли я прикрепить к спецификации генератор, который берет выходные данные исходного генератора и преобразует их в вектор?».


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


Ответы (3)


Создайте шаблон регулярного выражения независимо от спецификации:

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

(def pattern 
  (s/cat :sym symbol? :str string? :kws (s/* keyword?)))

(s/def ::solution
  (s/with-gen (s/and vector? pattern) 
              #(gen/fmap vec (spec/gen pattern))))

(s/valid? ::solution '(af "5" :key))  ;; false

(s/valid? ::solution ['af "5" :key])  ;; true

(gen/sample (s/gen ::solution) 4)
;; ([m ""] [. "" :Q] [- "" :?-/-9y :_7*/!] [O._7l/.?*+ "z" :**Q.tw.!_/+!gN :wGR/K :n/L])
person Alex Miller    schedule 05.04.2017
comment
У меня проблема с этим решением. Мы теряем «плоскостность» ``` (s/def ::pattern (s/cat :sym symbol? :str string? :kws (s/* ключевое слово?))) (s/def ::pattern-2 (s /cat :s string? :p ::pattern)) (s/valid? ::pattern ['af 5 :key]) ;; true (s/valid? ::pattern-2 [string 'af 5 :key]) ;; true (s/def ::pattern-3 (s/cat :s string? :p ::solution)) (s/valid? ::pattern-3 [string 'af 5 :key]) ;; ЛОЖЬ !!! ‹- мы теряем плоскостность (s/valid? ::pattern-3 [string ['af 5 :key]]) ;; истинный !!! ‹- мы теряем плоскостность ``` - person Lambder; 10.05.2017
comment
Да, это одна из проблем с описанным выше подходом - использование s/and изменит ::solution с операции регулярного выражения на спецификацию, и только операции регулярного выражения можно комбинировать простым способом. В настоящее время не существует одного простого решения, которое удовлетворяло бы всем потребностям кошек, использующих только переносчиков. - person Alex Miller; 10.05.2017
comment
@AlexMiller Как я могу сделать принуждение к вектору рекурсивным в генераторе? Например, для создания вложенных структур Hiccup. - person Roman Liutikov; 02.01.2018

Оказывается, нет тривиального способа решить эту проблему с clojure-1.9.0-alpha15. Одним из возможных решений является модификация генератора, чтобы преобразовать последовательность, заданную cat, в вектор, например:

(spec/def ::solution
  (let [s (spec/cat :sym symbol? :str string? :kws (spec/* keyword?))]
    (spec/with-gen s #(gen/fmap vec (spec/gen s)))))

который, как мы видим, производит и принимает правильные данные:

(spec/exercise ::solution)
=> ([[T ""] {:sym T, :str ""}]
    [[t* "Z" :g*] {:sym t*, :str "Z", :kws [:g*]}]
    [[G?8 "td" :*K/j] {:sym G?8, :str "td", :kws [:*K/j]}])

Хотя у него есть одна проблема, спецификация не проверяет, является ли ввод вектором, она принимает последовательности, такие как списки:

(spec/conform ::solution '(N-G.?8?4/- "" :G7y_.?Gx_/Oy1Dv :g!/Ooh0 :N-??h/o+cN))
=> {:sym N-G.?8?4/-, :str "", :kws [:G7y_.?Gx_/Oy1Dv :g!/Ooh0 :N-??h/o+cN]}
person Rovanion    schedule 05.04.2017

Чтобы добавить решение Алекса, вот макрос, который определяет операцию регулярного выражения vector-cat:

(defmacro vcat
  "Takes key+pred pairs, e.g.

  (vcat :e even? :o odd?)

  Returns a regex op that matches vectors, returning a map containing
  the keys of each pred and the corresponding value. The attached
  generator produces vectors."
  [& key-pred-forms]
  `(spec/with-gen (spec/and vector? (spec/cat ~@key-pred-forms))
     #(gen/fmap vec (spec/gen (spec/cat ~@key-pred-forms)))))
person Rovanion    schedule 07.04.2017