Fork me on GitHub
#clojure-spec
<
2017-11-07
>
mattiasw09:11:20

map-like? I have an application and I use maps a lot. Added spec, most of them using s/keys. In some cases I want to apply operations to the maps like filter. But this breaks the specs, since it isn't a map any more, but a sequence of pairs. The simple solution is of course to convert the result of filter back to a map, but is there a better way? Here is a small sample

(s/def ::test-id int?)
(s/def ::test-data string?)

(s/def ::test1
  (s/keys :req-un [::test-id ::test-data]))

(s/fdef spec-test
        :args (s/cat :m ::test1)
        :ret  int?)

(defn spec-test
  [m]
  (count m))

(def test1-sample {:test-id 1 :test-data "hello"})

(defn works
  []
  (spec-test test1-sample))

(defn works-not-which-is-ok
  []
  (spec-test (dissoc test1-sample :test-data)))

(defn works-not-which-is-not-ok
  []
  (spec-test (filter (fn [_] true) test1-sample)))

Alex Miller (Clojure team)13:11:22

You can spec them as s/coll-of an s/tuple of key value pairs. That’s not great if you are still relying on attribute keys. I guess you could also use s/keys* on the kv tuple.

mattiasw14:11:39

If I understand it correctly, s/keys* wants the structure [:a 1 :b 2], but I have [[:a 1][:b 2]]

Alex Miller (Clojure team)13:11:06

Yeah, you’d want coll-of keys*

rickmoynihan12:11:51

What is the best way to spec a map which has a single required key (s/keys :req-un [::id]) (s/def ::id int?) where every other (optional) map key is a string? with a string? value? Such that (s/valid ::spec {:id 123}) ;; => true, (s/valid ::spec {"foo" "bar" :id 123}) ;; => true, (s/valid ::spec {"foo" "bar"}) ;; => false

taylor13:11:17

here’s a really naive way to do it:

(s/def ::my-map
  (s/and (s/keys :req-un [::id])
         #(every? (fn [[k v]]
                    (or (= :id k)
                        (and (string? k) (string? v))))
                  %)))
maybe there’s a better way, but I’m not sure how you’d combine keys + map-of specs like this

Alex Miller (Clojure team)13:11:15

These are sometimes called hybrid maps - I have a blog about the spec for destructuring which covers the techniques for handling them. http://blog.cognitect.com/blog/2017/1/3/spec-destructuring

taylor13:11:47

very nice, then something kinda like this might work:

taylor13:11:59

(s/every (s/or :id (s/tuple #{:id} int?)
               :str (s/tuple string? string?)))

rickmoynihan13:11:49

taylor: yes I had something similar to your first, but the use of and & or in a single monolothic predicate bothered me, as it kills error message granularity further down the tree.

rickmoynihan13:11:22

I think as you’ve discovered the use of every and or looks to be how to do it 🙂

rickmoynihan13:11:33

Thanks @U064X3EF3 for the pro tips

sushilkumar12:11:44

How to write spec for “string of integer”? I want to create a generator of "string of integer" which should only generate valid string of integer (value within Integer/MIN_VALUE and Integer/MAX_VALUE).

taylor12:11:35

you could write a predicate function int-str? that returns true/false if the given string can be parsed as an integer, then it’s trivial to use that predicate as a spec

gfredericks12:11:59

The generator would be (gen/fmap str an-appropriate-int-generator)

sushilkumar14:11:58

Thanks @U3DAE8HMG and @U0GN0S72R for sharing your ideas. Now I am able to do it as follows and it will also help in writing fdefs.

(defn- in-integer? [x]
 (and (>= x Integer/MIN_VALUE) (<= x Integer/MAX_VALUE)))                      (s/def ::in-integer?
 (s/and number? in-integer?))           (s/def ::str-long?
 (s/spec string?
         :gen #(gen'/fmap str (s/gen ::in-long?))))

seancorfield18:11:55

Quick Q about double-in -- if I specify :min 0.0 and :max 1.0, does that automatically exclude NaN and infinity? Or do I also need to specify :NaN? false :infinity? false?

taylor18:11:24

looking at https://github.com/clojure/clojure/blob/d920ada9fab7e9b8342d28d8295a600a814c1d8a/src/clj/clojure/spec.clj#L1630 it doesn’t look like specifying min/max has any effect on NaN/infinity (and they both default to true)

taylor18:11:44

but

(s/valid? (s/double-in :min 0 :max 1) Double/NaN)
=> false

taylor18:11:28

so I guess it implicitly excludes NaN/infinity by virtue of those not passing the range comparator checks?

taylor19:11:06

(s/valid? (s/double-in :min 0.0) Double/POSITIVE_INFINITY)
=> true

seancorfield19:11:23

Thanks @U3DAE8HMG That's sort of what I intuitively expected to happen but I wasn't sure how "special" NaN was... I guess that begs the question of what do you do if you want 0.0 .. 1.0 or NaN? I suppose you have to :or two specs together... but what would that second spec look like, i.e., how would you allow only NaN or a range?

taylor19:11:14

hmm here’s my second naive stab today 🙂 bad code

taylor19:11:47

a more qualified person could very well have a better answer!

seancorfield19:11:50

That allows any double, it seems.

taylor20:11:10

ha! yeah it does… disregard

taylor20:11:47

looking at double-in impl. :NaN? true is a no-op

taylor20:11:53

(s/or :range (s/double-in :min 0.0 :max 1.0)
      :nan #(and (double? %) (Double/isNaN %)))