Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure spec: checking map values directly?

I'm writing a spec for a square, which is very simply a pair of integer coordinates (keys ::sq-x ::sq-y) combined with a sequential collection of vertices (key ::vtxs).

Speccing this constraint:

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

The above checks the presence of the keys only. To also check the key values, I add specs named the same as the keys to be checked. This implicit link between specs is always live:

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

In the above ::int-val is another spec checking value integer-ness (we are basically aliasing the specs: ::sq-x -> ::int-val):

(s/def ::int-val #(= (Math/floor %) (* 1.0 %)))

This works perfectly well. From another package, which imports the package above as sut ("system under test"), I can run this test code with err... "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))

So far so good.

Now, complication:

Prior to this I had tried to find a way to check the map values directly, without passing via another spec. I didn't find a way to make this palatable to Clojure. For example, this doesn't work:

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

Runtime is ouch time:

java.lang.IllegalArgumentException: No implementation of method:
   :specize* of protocol: #'clojure.spec.alpha/Specize found for class: nil

Ok. That code looks dodgy. Is there a way to reach into the map directly or am I always supposed to define another spec and call it implicitly through the naming?

like image 443
David Tonhofer Avatar asked Mar 10 '26 09:03

David Tonhofer


1 Answers

am I always supposed to define another spec and call it implicitly through the naming?

To use clojure.spec as intended/designed, the natural approach is to register your key specs as you've done here:

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

This gives "global" meaning to the keywords ::sq-x, ::sq-y, etc. Using this approach allows you to define a s/keys spec for a map with those keys:

(s/def ::square (s/keys :req [::sq-x ::sq-y ::vtxs]))

Then if you conform a map against ::square, spec will resolve each key's spec (if they exist in the spec registry) and conform each key's value respectively:

(s/conform ::square {::sq-x 1 ::sq-y 0 ::vtxs ["hey"]})

The intention here is to tie specs to strong names/keywords, so that ::sq-x means the same thing everywhere (though it's actually :whatever-namespace-foo/sq-x.

Is there a way to reach into the map directly

Yes, you can certainly define custom predicates/functions to inspect/conform whatever data you like. Your example above has a couple issues:

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

To get a better sense of that, try evaluating one of the forms individually and see that it evaluates to nil.

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

What you want instead is a function that will be passed some value and either return a value or perhaps :clojure.spec.alpha/invalid. This example would work without registering individual key specs, but I don't think it aligns well with spec's design:

(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 %))))
like image 196
Taylor Wood Avatar answered Mar 13 '26 19:03

Taylor Wood



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!