Clojure 规范:直接检查映射值?
Clojure spec: checking map values directly?
我正在为 square
编写规范,它非常简单地是一对整数坐标(键 ::sq-x
::sq-y
)与连续的顶点集合(键::vtxs
).
指定此约束:
(s/def ::square
(s/and
map? ; this is probably not needed
(s/keys :req [::sq-x ::sq-y ::vtxs])))
以上仅检查密钥是否存在。为了同时检查键 值 ,我添加了与要检查的键 同名的规范 。规范之间的隐式 link 始终有效:
(s/def ::sq-x ::int-val)
(s/def ::sq-y ::int-val)
(s/def ::vtxs sequential?)
上面的 ::int-val
是另一个规范检查值整数(我们基本上是规范的别名:::sq-x
-> ::int-val
):
(s/def ::int-val #(= (Math/floor %) (* 1.0 %)))
这非常有效。从另一个将上面的包导入为 sut
("system under test") 的包,我可以 运行 这个带有错误的测试代码... "good effect on target":
(t/deftest test-good-squares
(t/is (s/valid? ::sut/square
{ ::sut/sq-x 1 ::sut/sq-y -1 ::sut/vtxs [] }))
(t/is (s/valid? ::sut/square
{ ::sut/sq-x 5.0 ::sut/sq-y 5.0 ::sut/vtxs [] }))
(t/is (s/valid? ::sut/square
{ ::sut/sq-x 0.0 ::sut/sq-y 0.0 ::sut/vtxs [] })))
(t/deftest test-bad-squares-bad-coords
(t/is (not (s/valid? ::sut/square
{ ::sut/sq-x 1.1 ::sut/sq-y -1 ::sut/vtxs [] })))
(t/is (not (s/valid? ::sut/square
{ ::sut/sq-x -1 ::sut/sq-y 1.1 ::sut/vtxs [] })))
(t/is (not (s/valid? ::sut/square
{ ::sut/sq-x 1.1 ::sut/sq-y 1.1 ::sut/vtxs [] }))))
(t/deftest test-bad-squares-bad-vertexes
(t/is (not (s/valid? ::sut/square
{ ::sut/sq-x 1.1 ::sut/sq-y -1 ::sut/vtxs #{1 2 3} }))))
(t/deftest test-bad-squares-bad-type
(t/is (not (s/valid? ::sut/square [:a :b :c]))))
(t/deftest test-bad-squares-missing-keys
(t/is (not (s/valid? ::sut/square { ::sut/sq-y 0 ::sut/vtxs [] })))
(t/is (not (s/valid? ::sut/square { ::sut/sq-x 0 ::sut/vtxs [] })))
(t/is (not (s/valid? ::sut/square { ::sut/vtxs [] }))))
; call the above hierarchically
(t/deftest test-square
(test-good-squares)
(test-bad-squares-bad-coords)
(test-bad-squares-bad-vertexes)
(test-bad-squares-bad-type)
(test-bad-squares-missing-keys))
; call ONLY the test-square from "lein test", don't call individual
; tests a second time
(defn test-ns-hook [] (test-square))
到目前为止一切顺利。
现在,并发症:
在此之前,我试图找到一种直接检查地图值的方法,而不通过另一个规范。我没有找到让 Clojure 接受它的方法。例如,这不起作用:
(s/def ::square
(s/and
map?
(s/keys :req [::sq-x ::sq-y ::vtxs])
(::int-val #(get % ::sq-x))
(::int-val #(get % ::sq-y))
(sequential? #(get % ::vtxs))))
运行时间是 ouch 时间:
java.lang.IllegalArgumentException: No implementation of method:
:specize* of protocol: #'clojure.spec.alpha/Specize found for class: nil
好的。该代码看起来很狡猾。有没有办法直接进入地图,还是我总是应该定义另一个规范并通过命名隐式调用它?
我只想使用内置函数定义规范 int?
:
(s/def ::sq-x int?)
有关详细信息,请参阅:https://clojure.org/guides/spec#_composing_predicates。
但是,集合中的每个项目的规格都应该有一个 "type",因此规格可以重复使用。因此,::address
规范可能由 ::number
、::street
、::city
、::state
和 ::zip
组成。
参见:https://clojure.org/guides/spec#_entity_maps
更新:
我写了一个更通用的整数值测试函数:
(ns tst.demo.core
(:use demo.core tupelo.test)
(:require [tupelo.core :as t]))
(defn int-val?
"Returns true iff arg is an integer value of any Clojure/Java type
(all int types, float/double, BigInt/BigInteger, BigDecimal, clojure.lang.Ratio)."
[x]
(cond
(or (int? x) (integer? x)) true
; handles both java.lang.Float & java.lang.Double types
(float? x) (let [x-dbl (double x)] (= x-dbl (Math/floor x-dbl)))
(bigdecimal? x) (try
(let [bi-val (.toBigIntegerExact x)]
; no exception => fraction was zero
true)
(catch Exception e
; exception => fraction was non-zero
false))
(ratio? x) (zero? (mod x 1))
:else (throw (ex-info "Invalid type" {:x x}))))
(dotest
(is (not= 5 5.0))
(is (int-val? 5))
(is (int-val? 5.0))
(is (int-val? 5N))
(is (int-val? 5M))
(is (int-val? (bigdec 5)))
(is (int-val? (bigint 5)))
(is (int-val? (biginteger 5)))
(is (int-val? (* 3 (/ 5 3)) ))
(throws? (int-val? "five")))
am I always supposed to define another spec and call it implicitly through the naming?
要将 clojure.spec 用作 intended/designed,自然的方法是像您在此处所做的那样注册您的关键规格:
(s/def ::sq-x ::int-val)
(s/def ::sq-y ::int-val)
(s/def ::vtxs sequential?)
这为关键字 ::sq-x
、::sq-y
等赋予了 "global" 含义。使用这种方法允许您使用这些键为映射定义 s/keys
规范:
(s/def ::square (s/keys :req [::sq-x ::sq-y ::vtxs]))
然后,如果您根据 ::square
符合映射,spec 将解析每个键的规范(如果它们存在于 spec 注册表中)并分别符合每个键的值:
(s/conform ::square {::sq-x 1 ::sq-y 0 ::vtxs ["hey"]})
这里的目的是将规格与强 names/keywords 联系起来,这样 ::sq-x
在任何地方都意味着同样的事情(尽管它实际上是 :whatever-namespace-foo/sq-x
.
Is there a way to reach into the map directly
是的,您当然可以将自定义 predicates/functions 定义为 inspect/conform 任何您喜欢的数据。您上面的示例有几个问题:
(s/def ::square
(s/and
map? ;; unnecessary with s/keys
(s/keys :req [::sq-x ::sq-y ::vtxs])
;; the following forms don't evaluate to functions, so they aren't used as predicates
(::int-val #(get % ::sq-x))
(::int-val #(get % ::sq-y))
(sequential? #(get % ::vtxs))))
为了更好地理解这一点,请尝试单独评估其中一种形式,看看它的计算结果是否为零。
user=> (::int-val #(get % ::sq-x))
nil
你想要的是一个函数,它将传递一些值和 return 一个值或者 :clojure.spec.alpha/invalid
。这个例子可以在不注册单个关键规范的情况下工作,但我认为它不符合规范的设计:
(s/def ::square
(s/and
(s/keys :req [::sq-x ::sq-y ::vtxs])
#(= (Math/floor (::sq-x %)) (* 1.0 (::sq-x %)))
#(= (Math/floor (::sq-y %)) (* 1.0 (::sq-y %)))
#(sequential? (::vtxs %))))
我正在为 square
编写规范,它非常简单地是一对整数坐标(键 ::sq-x
::sq-y
)与连续的顶点集合(键::vtxs
).
指定此约束:
(s/def ::square
(s/and
map? ; this is probably not needed
(s/keys :req [::sq-x ::sq-y ::vtxs])))
以上仅检查密钥是否存在。为了同时检查键 值 ,我添加了与要检查的键 同名的规范 。规范之间的隐式 link 始终有效:
(s/def ::sq-x ::int-val)
(s/def ::sq-y ::int-val)
(s/def ::vtxs sequential?)
上面的 ::int-val
是另一个规范检查值整数(我们基本上是规范的别名:::sq-x
-> ::int-val
):
(s/def ::int-val #(= (Math/floor %) (* 1.0 %)))
这非常有效。从另一个将上面的包导入为 sut
("system under test") 的包,我可以 运行 这个带有错误的测试代码... "good effect on target":
(t/deftest test-good-squares
(t/is (s/valid? ::sut/square
{ ::sut/sq-x 1 ::sut/sq-y -1 ::sut/vtxs [] }))
(t/is (s/valid? ::sut/square
{ ::sut/sq-x 5.0 ::sut/sq-y 5.0 ::sut/vtxs [] }))
(t/is (s/valid? ::sut/square
{ ::sut/sq-x 0.0 ::sut/sq-y 0.0 ::sut/vtxs [] })))
(t/deftest test-bad-squares-bad-coords
(t/is (not (s/valid? ::sut/square
{ ::sut/sq-x 1.1 ::sut/sq-y -1 ::sut/vtxs [] })))
(t/is (not (s/valid? ::sut/square
{ ::sut/sq-x -1 ::sut/sq-y 1.1 ::sut/vtxs [] })))
(t/is (not (s/valid? ::sut/square
{ ::sut/sq-x 1.1 ::sut/sq-y 1.1 ::sut/vtxs [] }))))
(t/deftest test-bad-squares-bad-vertexes
(t/is (not (s/valid? ::sut/square
{ ::sut/sq-x 1.1 ::sut/sq-y -1 ::sut/vtxs #{1 2 3} }))))
(t/deftest test-bad-squares-bad-type
(t/is (not (s/valid? ::sut/square [:a :b :c]))))
(t/deftest test-bad-squares-missing-keys
(t/is (not (s/valid? ::sut/square { ::sut/sq-y 0 ::sut/vtxs [] })))
(t/is (not (s/valid? ::sut/square { ::sut/sq-x 0 ::sut/vtxs [] })))
(t/is (not (s/valid? ::sut/square { ::sut/vtxs [] }))))
; call the above hierarchically
(t/deftest test-square
(test-good-squares)
(test-bad-squares-bad-coords)
(test-bad-squares-bad-vertexes)
(test-bad-squares-bad-type)
(test-bad-squares-missing-keys))
; call ONLY the test-square from "lein test", don't call individual
; tests a second time
(defn test-ns-hook [] (test-square))
到目前为止一切顺利。
现在,并发症:
在此之前,我试图找到一种直接检查地图值的方法,而不通过另一个规范。我没有找到让 Clojure 接受它的方法。例如,这不起作用:
(s/def ::square
(s/and
map?
(s/keys :req [::sq-x ::sq-y ::vtxs])
(::int-val #(get % ::sq-x))
(::int-val #(get % ::sq-y))
(sequential? #(get % ::vtxs))))
运行时间是 ouch 时间:
java.lang.IllegalArgumentException: No implementation of method:
:specize* of protocol: #'clojure.spec.alpha/Specize found for class: nil
好的。该代码看起来很狡猾。有没有办法直接进入地图,还是我总是应该定义另一个规范并通过命名隐式调用它?
我只想使用内置函数定义规范 int?
:
(s/def ::sq-x int?)
有关详细信息,请参阅:https://clojure.org/guides/spec#_composing_predicates。
但是,集合中的每个项目的规格都应该有一个 "type",因此规格可以重复使用。因此,::address
规范可能由 ::number
、::street
、::city
、::state
和 ::zip
组成。
参见:https://clojure.org/guides/spec#_entity_maps
更新:
我写了一个更通用的整数值测试函数:
(ns tst.demo.core
(:use demo.core tupelo.test)
(:require [tupelo.core :as t]))
(defn int-val?
"Returns true iff arg is an integer value of any Clojure/Java type
(all int types, float/double, BigInt/BigInteger, BigDecimal, clojure.lang.Ratio)."
[x]
(cond
(or (int? x) (integer? x)) true
; handles both java.lang.Float & java.lang.Double types
(float? x) (let [x-dbl (double x)] (= x-dbl (Math/floor x-dbl)))
(bigdecimal? x) (try
(let [bi-val (.toBigIntegerExact x)]
; no exception => fraction was zero
true)
(catch Exception e
; exception => fraction was non-zero
false))
(ratio? x) (zero? (mod x 1))
:else (throw (ex-info "Invalid type" {:x x}))))
(dotest
(is (not= 5 5.0))
(is (int-val? 5))
(is (int-val? 5.0))
(is (int-val? 5N))
(is (int-val? 5M))
(is (int-val? (bigdec 5)))
(is (int-val? (bigint 5)))
(is (int-val? (biginteger 5)))
(is (int-val? (* 3 (/ 5 3)) ))
(throws? (int-val? "five")))
am I always supposed to define another spec and call it implicitly through the naming?
要将 clojure.spec 用作 intended/designed,自然的方法是像您在此处所做的那样注册您的关键规格:
(s/def ::sq-x ::int-val)
(s/def ::sq-y ::int-val)
(s/def ::vtxs sequential?)
这为关键字 ::sq-x
、::sq-y
等赋予了 "global" 含义。使用这种方法允许您使用这些键为映射定义 s/keys
规范:
(s/def ::square (s/keys :req [::sq-x ::sq-y ::vtxs]))
然后,如果您根据 ::square
符合映射,spec 将解析每个键的规范(如果它们存在于 spec 注册表中)并分别符合每个键的值:
(s/conform ::square {::sq-x 1 ::sq-y 0 ::vtxs ["hey"]})
这里的目的是将规格与强 names/keywords 联系起来,这样 ::sq-x
在任何地方都意味着同样的事情(尽管它实际上是 :whatever-namespace-foo/sq-x
.
Is there a way to reach into the map directly
是的,您当然可以将自定义 predicates/functions 定义为 inspect/conform 任何您喜欢的数据。您上面的示例有几个问题:
(s/def ::square
(s/and
map? ;; unnecessary with s/keys
(s/keys :req [::sq-x ::sq-y ::vtxs])
;; the following forms don't evaluate to functions, so they aren't used as predicates
(::int-val #(get % ::sq-x))
(::int-val #(get % ::sq-y))
(sequential? #(get % ::vtxs))))
为了更好地理解这一点,请尝试单独评估其中一种形式,看看它的计算结果是否为零。
user=> (::int-val #(get % ::sq-x))
nil
你想要的是一个函数,它将传递一些值和 return 一个值或者 :clojure.spec.alpha/invalid
。这个例子可以在不注册单个关键规范的情况下工作,但我认为它不符合规范的设计:
(s/def ::square
(s/and
(s/keys :req [::sq-x ::sq-y ::vtxs])
#(= (Math/floor (::sq-x %)) (* 1.0 (::sq-x %)))
#(= (Math/floor (::sq-y %)) (* 1.0 (::sq-y %)))
#(sequential? (::vtxs %))))