Clojure spec:直接检查地图值吗?

时间:2019-09-05 17:40:19

标签: clojure clojure.spec

我正在为square写一个规范,它非常简单,是一对整数坐标(键::sq-x ::sq-y)与顶点的顺序集合(键{{ 1}})。

说明此约束:

::vtxs

以上内容仅检查密钥的存在。为了还检查键,我添加了规范,其名称与要检查的键相同。规格之间的这种隐式链接始终有效:

(s/def ::square
   (s/and
      map? ; this is probably not needed
      (s/keys :req [::sq-x ::sq-y ::vtxs])))

在上面的(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 %))) (“被测系统”)的程序包中,我可以运行此测试代码,并显示err ...“对目标效果良好”:

sut

到目前为止很好。

现在,并发症:

在此之前,我试图找到一种方法来直接检查地图值,而无需通过其他规范。我没有找到使Clojure变得可口的方法。例如,这不起作用:

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

运行时间是非常宝贵的时间:

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

好的。该代码看起来很狡猾。有没有办法直接进入地图,还是我总是应该定义另一个规范并通过命名隐式调用它?

2 个答案:

答案 0 :(得分:3)

  

我总是应该定义另一个规范并通过命名隐式调用它吗?

要按预期/设计目的使用clojure.spec,自然的方法是按照此处的步骤注册关键规格:

(s/def ::sq-x ::int-val)
(s/def ::sq-y ::int-val)
(s/def ::vtxs sequential?)

这为关键字::sq-x::sq-y等赋予了“全局”含义。使用这种方法,您可以使用这些键为地图定义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"]})

这里的目的是将规范与强名称/关键字相关联,以便::sq-x到处都具有相同的含义(尽管实际上是:whatever-namespace-foo/sq-x

  

有没有办法直接进入地图

是的,您当然可以定义自定义谓词/函数来检查/整合所需的任何数据。上面的示例有两个问题:

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

要更好地理解这一点,请尝试分别评估其中一种形式,并查看结果为nil。

user=> (::int-val #(get % ::sq-x))
nil

您想要的是一个将传递一些值并返回值或返回: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 %))))

答案 1 :(得分:2)

我只是使用内置函数int?定义规格:

(s/def ::sq-x int?)

有关详细信息,请参见:https://clojure.org/guides/spec#_composing_predicates

但是,规范要求集合中的每个项目都具有一个“类型”,因此规范可以重复使用。因此,::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")))