匹配并生成可变长度的有序向量的 Clojure 规范
A Clojure Spec that matches and generates an ordered vector of variable length
让我们从
的常规序列开始
(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]]] ...)
我们还可以尝试使用 spec/and
:
向 ::cat
添加附加条件
(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]
这样的向量的规范?
也可以将问题改写为 "Is there an alternative to spec/cat
which generates vectors instead of lists?" 或 "Is it possible to pass a :kind argument to spec/cat
?" 或 "Can I attach a generator to a spec which takes the output of the original generator and casts it to a vector?"。
事实证明,从 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]}
独立于规范创建正则表达式模式:
(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])
要添加 Alex 的解决方案,这里有一个定义 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)))))
让我们从
的常规序列开始(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]]] ...)
我们还可以尝试使用 spec/and
:
::cat
添加附加条件
(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]
这样的向量的规范?
也可以将问题改写为 "Is there an alternative to spec/cat
which generates vectors instead of lists?" 或 "Is it possible to pass a :kind argument to spec/cat
?" 或 "Can I attach a generator to a spec which takes the output of the original generator and casts it to a vector?"。
事实证明,从 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]}
独立于规范创建正则表达式模式:
(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])
要添加 Alex 的解决方案,这里有一个定义 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)))))