Fork me on GitHub
#clojure-spec
<
2018-10-10
>
ScottStackelhouse16:10:21

Hi all, I don’t slack much. I have a case where I model data as a map, some keys required, some not. I already use one key as a “type” or “kind” indicator. The problem I have is I have a set of keys where all are optional, but at least one of them must be present.

ScottStackelhouse16:10:34

I have not had luck figuring out how to spec that

ScottStackelhouse16:10:49

Any thoughts or links to point me at?

Alex Miller (Clojure team)16:10:03

use s/and to combine a predicate that checks that condition

ScottStackelhouse16:10:54

Mostly the problems I run ino with spec’ing like that is it seems to imply/expect more hierarchy than o have

ScottStackelhouse16:10:12

I’m sure it’s user error, but examples are hard to come by

piotr-yuxuan16:10:39

Sorry for disrupting your chat. I’m wondering how to programmatically register a spec.

(defmacro draft-make-spec
  [spec-name spec-pred]
  (eval `(list 'spec/def ~spec-name ~spec-pred)))

(def spec-name ::g)
(def spec-pred int?)
(draft-make-spec spec-name spec-pred)
(spec/valid? ::g 4)
;; works correctly
;; => true

(let [spec-name ::h]
  (draft-make-spec spec-name int?)
  (spec/valid? ::h 4))
;; => throws in java.lang.InstantiationException

Alex Miller (Clojure team)16:10:07

@scottstack (s/and (s/keys :opt [::a ::b ::c]) #(some #{::a ::b ::c} (keys %)))

Alex Miller (Clojure team)16:10:18

@piotr2b you’re doing too much there I think

ScottStackelhouse16:10:25

That’s interesting. I tried something similar with s/alt but didn’t quite get there.

ScottStackelhouse16:10:45

Thanks @alexmiller , I’ll see where I get with that. It at least tells me I’m heading in the right direction.

piotr-yuxuan16:10:55

@alexmiller, at first I thought it would as easy as this:

(defmacro draft-make-spec
  [spec-name spec-pred]
  `(spec/def ~spec-name ~spec-pred))
but it doesn’t work:
(let [spec-name ::h]
  (draft-make-spec spec-name int?)
  (spec/valid? ::h 4))
;; => CompilerException java.lang.Exception: Unable to resolve spec: ::h
😞 any tiny help from you would be greatly appreciated! 💪

piotr-yuxuan16:10:58

I was wondering how I could write a small library to generate specs from avro schema, so I need to call spec/def in a let and I only know the spec name at runtime.

ScottStackelhouse16:10:53

@piotr2b have you looked at the macro expansions of your macro and of s/def? I did a quick try and it didn’t come out how I expected.

Alex Miller (Clojure team)16:10:24

I’ve done some stuff like this elsewhere but can’t put my finger on it right now and I need to log off, sorry

ScottStackelhouse16:10:16

I put the macro expansion of the s/def form into the let in place of the macro call, ie (s/def-impl ‘spec-name ‘int? int?) and it fails an assertion that “k” is a keyword...

ScottStackelhouse16:10:36

Nomenclature fails me but it is like spec-name in your macro needs a double deref

ScottStackelhouse16:10:33

@piotr2b If you take the expansion of s/def, and use that in your draft-make-spec macro, it works

piotr-yuxuan16:10:54

Could you show me that? 😄

piotr-yuxuan16:10:03

It’s funny because we’ve swapped problems

piotr-yuxuan16:10:09

I was working on solving yours

ScottStackelhouse16:10:10

Yeah, on my phone tho

piotr-yuxuan16:10:29

Here is what I’ve got so far:

(spec/def :entity/kind #{:entity/car :entity/person})
(spec/def :person/name string?)
(spec/def :person/age pos-int?)
(spec/def :car/model-name string?)
(spec/def :car/age pos-int?)

(spec/def ::scottstack-spec
  (and (spec/keys :opt-un [:entity/kind
                           :person/name
                           :person/age
                           :car/model-name
                           :car/age])
       #(condp = (:entity/kind %)
          :entity/car (every? (set (keys %)) #{:car/model-name :car/age})
          :entity/person (every? (set (keys %)) #{:person/name :person/age})
          false)))

(spec/valid? ::scottstack-spec {:entity/kind :entity/car
                                :person/name "scottstack"
                                :person/age 21})
;; => false

(spec/valid? ::scottstack-spec {:entity/kind :entity/person
                                :person/name "scottstack"
                                :person/age 21})
;; => true

piotr-yuxuan16:10:36

Does it suit your need?

ScottStackelhouse16:10:23

I will have to check

piotr-yuxuan16:10:27

Basically I’m a bit new with macros, so if you could write down the code in addition to your previous explanations, that would be wonderful 🙂

ScottStackelhouse16:10:53

Let me switch to something with a keyboard

piotr-yuxuan16:10:10

@alexmiller how sad! if you could find it back it would be awesome. Anyway, thanks for everything you do for this amazing language 🙂

piotr-yuxuan16:10:33

@scottstack, I fixed a typo, it’s better with :opt-un

piotr-yuxuan17:10:56

And the last test case:

(spec/valid? ::scottstack-spec {:entity/kind :entity/person
                                :person/name "scottstack"})
;; => false

ScottStackelhouse17:10:58

(defmacro draft-make-spec [spec-name spec-pred] `(s/def-impl spec-name ‘spec-pred ~spec-pred)) (let [sname ::something] (draft-make-spec sname int?) (s/valid? ::something 4)

ScottStackelhouse17:10:46

I am very good with macros either. I depend heavily on cider's macroexpansion

ScottStackelhouse17:10:21

S/def would expand to (s/def-impl 'sname 'int? int?)

ScottStackelhouse17:10:02

And that would fail an assertion that the first arg be a keyword

ScottStackelhouse17:10:37

I see I goofed up, maybe it still doesn't work

ScottStackelhouse17:10:55

Nevermind, I just confused myself momentarily... I think it is ok

firstclassfunc19:10:13

Afternoon, is anyone aware of any projects to transform JSON Schema into Clojure Spec?

lilactown19:10:46

I don't know of any projects for doing JSON Schema => clojure.spec

lilactown19:10:01

spec-tools can go the other way: clojure.spec => JSON Schema

firstclassfunc19:10:41

@lilactown Yea tks.. Do people think that would be valuable? There are lots of standards defined in terms of JSON Schema and would be beneficial to auto-generate specs so they can be consumes without the tedious nature of crafting data structures from scratch

lilactown19:10:00

I think it could be pretty useful to help bootstrap some of the commonly needed specs, yeah 😄

devn21:10:08

This may be a test.check question more than a spec question, but here goes: I have a map named foo: {:a 1 :b nil :c 32} If a is present, then b should be nil, and c should be a pos-int?. {:a nil :b true :c nil} When a is nil, c must also be nil, and b must hold a value. I want to be able to generate examples of both types of maps.

devn21:10:37

Perhaps I should be tagging them as :type :a and :type :b and using a multi-spec?

gfredericks22:10:20

As a pure t.c question it's easy - use gen/one-of; not sure about making that more spectomatic

gfredericks22:10:47

multi-spec sounds plausible but I am not a dentist

devn22:10:32

The way I explained it above is kind of crappy:

(s/def :my/a #{1 2 nil})
(s/def :my/b (s/or :has-b pos-int? :no-b nil?))
(s/def ::thing (s/keys :req-un [:my/a :my/b])
example:
(let [a-thing {:a 1 :b nil}
  (when (and (:a a-thing) (not (:b a-thing))) (println "it's a foo")
  (when (and (not (:a a-thing)) (:b a thing)) (println "it's a bar")))
I think I need some such-that magic maybe?

devn22:10:25

So what I want is if I then ran (gen/sample (s/gen ::thing)), I'd never get back records which have both :a and :b populated, only one or the other.

gfredericks22:10:19

Have you tried writing a map spec for each case and using s/or?

devn22:10:34

no but that makes perfect sense

devn22:10:13

just to be clear, what I think you're suggesting is this:

(s/def :myA/a #{1 2})
(s/def :myA/b nil?)

(s/def :myB/a nil?)
(s/def :myB/b pos-int?)

(s/def ::thing-a (s/keys :req-un [:myA/a :myA/b])
(s/def ::thing-b (s/keys :req-un [:myB/a :myB/b])
(s/def ::either-thing (s/or ::thing-a ::thing-b))

devn22:10:01

follow on question is then perhaps, how to tune the distribution of thing-a and thing-b's generated

bbrinck22:10:30

haven’t tried it, but my guess in a custom generator (See s/with-gen) + http://clojure.github.io/test.check/clojure.test.check.generators.html#var-frequency

devn22:10:43

(gen/sample (gen/frequency [[2 (s/gen ::thing-a)]
                                      [2 (s/gen ::thing-b)]])
                      10)
perhaps?

devn22:10:27

though you may be on to something with with-gen

devn22:10:01

it would be nice to attach the frequency to the spec

bbrinck22:10:10

untested, but then you can do something like (s/def ::either-thing (s/with-gen (s/or ::thing-a ::thing-b) #(gen/frequency [[2 (s/gen ::thing-a)] [2 (s/gen ::thing-b)]]) ))

devn22:10:32

that doesn't seem to do it

bbrinck22:10:37

hm, probably a bug in my code, but in general with-gen should override the default generator. remember the function needs to return a generator

bbrinck22:10:02

IOW, the second arg can’t be a generator

devn22:10:11

yes, which is indeed what your code does there

devn22:10:48

maybe gary's previous suggestion to use one-of is worth trying here

bbrinck22:10:13

what doesn’t work with frequencies?

devn22:10:23

i tried changing the frequency from 2 to 0 for one of them, and it doesn't vary the samples

devn22:10:35

they all are ::thing-a-like things

devn22:10:51

whereas the thing I posted above does work

bbrinck22:10:23

well, if you put 0 for thing-b, then everything should be thing-a, yes?

devn22:10:36

yes, but i tried it both ways, and the same result

devn22:10:41

tested again to make sure im not crazy

devn22:10:58

ExceptionInfo Couldn't satisfy such-that predicate after 100 tries.

devn22:10:14

is what I wind up with when I try to make it return only one of the types, so perhaps something else is amiss here

bbrinck22:10:40

hmmm, yes, i’m trying a simpler example and not seeing what i expect, so I’m clearly missing something

devn22:10:47

@bbrinck here's a thing: if i switch the order of the s/or, it works in one direction, but not the other

devn22:10:04

bah, ok, im an idiot

devn22:10:35

(s/or ::thing-a ::thing-b) != (s/or :a ::thing-a :b ::thing-b)

bbrinck22:10:54

haha wow I missed that too

bbrinck22:10:13

just trying to figure out why my simple example wasn’t even conforming

bbrinck22:10:27

good catch

bbrinck22:10:03

ok, now with-gen + frequency is working again for my simple example.

(s/def ::name string?)
(s/def ::age int?)
(s/def ::either (s/with-gen (s/or :name ::name :age ::age) #(gen/frequency [[4 (s/gen ::name)] [1 (s/gen ::age)]])))
(s/exercise ::either)

bbrinck22:10:08

is your example working now?

devn22:10:20

yeah, works good

👍 4
devn22:10:36

chef kisses fingers

devn22:10:21

thanks for your help @bbrinck

devn22:10:54

(my/or :name ::name 1 :age ::age 2) or somesuch

bbrinck22:10:46

maybe not easy, but easier if you define the syntax with a regexp spec and conform 🙂

devn23:10:31

ok i spent 10min on it and the sugar ain't worth it 😄

devn23:10:17

(defmacro myor [& key-pred-freq-forms]
  `(s/with-gen (s/or ~@(flatten (for [[k# p# _] (partition 3 key-pred-freq-forms)]
                                  [k# p#])))
     (fn [] (gen/frequency ~(into [] (for [[k# _ f#] (partition 3 key-pred-freq-forms)]
                                       [f# (s/gen p#)]))))))
or something, but :man-shrugging: