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 %))))