简体   繁体   中英

clojure spec for hash-map with interdependent values?

I want to write a clojure spec for a hash-map wherein the value of one of the keys is constrained to be equal to the sum of the values of two other keys. I know one way to write a test generator for such a spec by hand:

(ns my-domain)
(require '[clojure.test           :refer :all     ]
         '[clojure.spec.alpha     :as s           ]
         '[clojure.spec.gen.alpha :as gen         ]
         '[clojure.pprint         :refer (pprint) ])

(s/def ::station-id string?)
(s/def ::sim-time (s/double-in :infinite? true, :NaN? false))
(s/def ::reserved-counts (s/and int? #(not (neg? %))))
(s/def ::free-counts     (s/and int? #(not (neg? %))))

(def counts-preimage (s/gen (s/keys :req [::station-id
                                          ::sim-time
                                          ::reserved-counts
                                          ::free-counts])))

(pprint (gen/generate
         (gen/bind
          counts-preimage
          #(gen/return
            (into % {::total-counts
                     (+ (::reserved-counts %)
                        (::free-counts %))})))))
 #:my-domain{:station-id "sHN8Ce0tKWSdXmRd4e46fB", :sim-time -3.4619293212890625, :reserved-counts 58, :free-counts 194, :total-counts 252} 

But I haven't figured out how to write a spec for it, let alone a spec that produces a similar generator. The gist of the problem is that I lack, in the space of specs, a way to get hold of the "preimage" in the spec, that is, I lack an analogue to bind from the space of generators. Here is a failed attempt:

(s/def ::counts-partial-hash-map
  (s/keys :req [::station-id
                ::sim-time
                ::reserved-counts
                ::free-counts]))
(s/def ::counts-attempted-hash-map
  (s/and ::counts-partial-hash-map
         #(into % {::total-counts (+ (::reserved-counts %)
                                     (::free-counts %))})))

(pprint (gen/generate (s/gen ::counts-attempted-hash-map)))
 #:my-domain{:station-id "ls5qBUoF", :sim-time ##Inf, :reserved-counts 56797960, :free-counts 17} 

The generated sample conforms to the spec because #(into % {...}) is truthy, but the result doesn't contain the new attribute with the key ::total-counts .

I'd be grateful for any guidance.

EDIT : Today I Learned about s/with-gen , which will allow me to attach my (working) test generator to my "preimage" or "partial" spec. Perhaps that's the best way forward?

You could use the nat-int? predicate (for which there's a built-in spec, thanks @glts) for the count keys, and add a ::total-counts spec too:

(s/def ::reserved-counts nat-int?)
(s/def ::free-counts nat-int?)
(s/def ::total-counts nat-int?)

(s/def ::counts-partial-hash-map
  (s/keys :req [::station-id
                ::sim-time
                ::reserved-counts
                ::free-counts]))

spec for a hash-map wherein the value of one of the keys is constrained to be equal to the sum of the values of two other keys

To add this assertion you can s/and a predicate function with the keys spec (or in this example the merge spec that merges the partial map spec with a ::total-count keys spec):

(s/def ::counts-attempted-hash-map
  (s/with-gen
    ;; keys spec + sum-check predicate
    (s/and
      (s/merge ::counts-partial-hash-map (s/keys :req [::total-counts]))
      #(= (::total-counts %) (+ (::reserved-counts %) (::free-counts %))))
    ;; custom generator
    #(gen/fmap
       (fn [m]
         (assoc m ::total-counts (+ (::reserved-counts m) (::free-counts m))))
       (s/gen ::counts-partial-hash-map))))

This also uses with-gen to associate a custom generator with the spec that sets ::total-count to the sum of the other count keys.

(gen/sample (s/gen ::counts-attempted-hash-map) 1)
=> (#:user{:station-id "", :sim-time 0.5, :reserved-counts 1, :free-counts 1, :total-counts 2})

The generated sample conforms to the spec because #(into % {...}) is truthy, but the result doesn't contain the new attribute with the key ::total-counts .

I'd recommend against using specs to calculate/add ::total-counts to the map. Specs generally shouldn't be used for data transformation.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM