Fork me on GitHub
#clojure-spec
<
2017-01-05
>
bbloom04:01:16

so when we’re not using generative tests, what do people use for utilizing specs from tests?

bbloom04:01:24

i find s/assert annoying b/c of the check-asserts compile time flag - need to forcibly reload everything that might have asserts when working interactively

bbloom04:01:27

instrument is nicely dynamic, but i understand why the asserts are static

bbloom04:01:50

i still don’t really understand why :ret and :fn are not checked when instrumented

bbloom04:01:29

The guide now says "Note that the :ret and :fn specs are not checked with instrumentation as validating the implementation should occur at testing time.” - but instrument are in the stest namespace, soo presumably you only instrument at test time anyway?

bbloom04:01:27

I get the desire to not instrument ret and fn for other peoples code in some cases, but honestly, i also want to validate that the libraries i’m calling respect their specs

bbloom04:01:29

i don’t trust them

bbloom04:01:24

i’m just going to call s/assert* for manual (non-generative) tests and deal with it if it breaks at some point 😉

dacopare12:01:38

Hello, I've just started using Spec! I'm trying to use it in a particular situation that I can't see covered in the documentation but I may be wrong. I have two datastructures:

(def created
 {:event-type "TxnCreated"
  :data {:id "foo"
         :amount 6}})

(def deleted
 {:event-type "TxnDeleted"
  :data {:id "foo"
         :reason "bar"}})

dacopare12:01:50

I'd like to have a different spec for each of the different :data items, but from what I can see s/keys will use one spec for one key...

dacopare12:01:00

So that a deleted item would have different required keys to a created item. Is the problem that my keys are not namespaced? Or is this going against: > "the map spec never specifies the value spec for the attributes, only what attributes are required or optional"

dacopare12:01:50

Thank you for any help simple_smile

manutter5113:01:03

Yeah, this is exactly the use case for namespaces. You want to re-use the same keyword to mean 2 different things, put them in different namespaces and you’ll be fine

dacopare13:01:29

@manutter51 the only problem I have with this is that I currently have functions that accept either of these structures, and rely on the presence of the data key.

thomas13:01:39

Hi, just as an idea/question I had... is it possible the do clever things with date/times in spec. I assume you can say that a certain value should be an date/time instance... but wouldn't it be nice if you could express in a spec that a certain date/time should come before/after a different date/time ?

thomas13:01:41

for instance {:start #inst 2017-01-01 :end #inst 2017-01-5} and if the end is before the start it fails to validate?

mpenet14:01:37

I guess you can do that with s/and

mpenet14:01:28

(s/and (s/keys ....) #(start<end? %)) where the latter function checks the values

thomas14:01:47

ok cool @mpenet that looks like a good solution.

mpenet14:01:50

(s/def ::fancy (s/and (s/keys :req-un [::start ::end]) (fn [{:keys [start end]}] (> end start))))
:user/fancy
user> (s/def ::start int?)
:user/start
user> (s/def ::end int?)
:user/end
user> (s/valid? ::fancy {:start 0 :end 1})
true
user> (s/valid? ::fancy {:start 0 :end -1})
false
user> (s/valid? ::fancy {:start 1 :end 0})
false

thomas14:01:25

aah cool... I hadn't realised you can attach the function to the spec. that is very handy indeed

mpenet14:01:53

specs are just predicates really

mpenet14:01:04

well the building blocks at least

Alex Miller (Clojure team)14:01:48

that is, no not in any way obvious to me :)

Yehonathan Sharvit14:01:39

One reproduction of the issue is shown by @bbloom

(s/unform :clojure.core.specs/defn-args (s/conform :clojure.core.specs/defn-args '(f [& xs])))
;;; (f ((& xs)))

Alex Miller (Clojure team)14:01:29

oh, just another case. the bug is in spec itself, not in what’s in the post.

Yehonathan Sharvit14:01:18

I never meant that there was a bug in the post

Alex Miller (Clojure team)14:01:43

well, not sure why you’re asking then

Yehonathan Sharvit14:01:56

I’m asking because after reading your post, people might try to do a “conform unform” loop and they might be surprised by the result...

martinklepsch15:01:50

I’ve been using collection-check https://github.com/ztellman/collection-check a while ago — is something like that becoming easier with spec? Maybe there are generators etc for ·built-in data structures?

joshjones15:01:00

@dacopare Given your data, you can still use unqualified keys in your existing data. The spec keys must be qualified, but not the keys in your map. They would look something like this:

(s/def :generic/event-type string?)
(s/def :generic/id string?)
(s/def :created/amount pos-int?)
(s/def :deleted/reason string?)

(s/def :created/data (s/keys :req-un [:generic/id :created/amount]))
(s/def :deleted/data (s/keys :req-un [:generic/id :deleted/reason]))

(s/def :created/event (s/keys :req-un [:generic/event-type :created/data]))
(s/def :deleted/event (s/keys :req-un [:generic/event-type :deleted/data]))

(s/conform :created/event created)
(s/conform :deleted/event deleted)

Alex Miller (Clojure team)15:01:41

@martinklepsch I’m not sure spec adds much over collection-check, which has a pretty specific purpose. spec does allow you to gen from collection specs, but not sure if that’s easier. depends what you’re trying to do.

thegeez15:01:49

@alexmiller in the blogpost the result for the 3rd "WORK IN PROGRESS" block should be: (s/conform ::seq-binding-form '[a b & r :as s]) {:elems [a b], :rest {:amp &, :form r}, :as {:as :as, :sym s}}

martinklepsch15:01:25

@alexmiller I basically just want to check if a thing behaves like a set, a vector, a list. Guess collection-check it still is then 🙂 Thought maybe through specs for core it would have become easier to generate operations

Alex Miller (Clojure team)15:01:48

yeah, nothing at the level of collection-check

bbloom18:01:10

one thing i learned from your blog post was that simple-symbol? existed 🙂 i had defined my own unqualified-symbol?

manderson18:01:30

500 fake internet points to the person that spots the problem (took me all morning to debug this and thought it'd be more fun to make it a challenge simple_smile )

(spec/fdef run-thread!
           :args (spec/cat :fn (spec/fspec :args empty? :ret nil?)
                           :name string?)
           :ret #(instance? Thread %))

(defn run-thread!
  [f n]
  (let [t (Thread. ^Runnable f ^String n)]
    (.start t)
    t))

(defn my-runnable
  []
  (try
    (while true
      (Thread/sleep 1000)
      (println "Hello, World!"))
    (catch Exception e
      (println "Got an exception!" (.getMessage e) "Exiting..."))))

(clojure.spec.test/instrument)

(def t (run-thread! my-runnable "my-thread"))

bfabry18:01:24

name is optional in your fdef but not in your function?

manderson18:01:14

Ah, good catch. That's an artifact of copy/paste (my original run-thread! had 2 arities where name was optional). But, that's not the issue that got me...

manderson18:01:42

fixed it 🙂

manderson18:01:56

I think that's worth 50 fake internet points, though 😉

manderson18:01:28

hint: it is related to spec...

bfabry18:01:29

other than that seems to work for me. except of course there's not really any reason why either sleep or println would throw an exception

manderson18:01:55

well, the catch exception is for killing the thread with (.interrupt t)

manderson18:01:26

try pasting the function definitions in the repl first and then hit enter. after that paste the instrument and def separately...

manderson18:01:14

(just to make sure you are instrumenting on the newly added run-thread! spec)

bfabry18:01:41

doesn't that need to be `run-thread! for that?

bfabry19:01:12

iunno, nothing particularly interesting is happening for me

manderson19:01:35

Some more detail: What this does is runs the my-runnable in the current REPL thread and never returns from run-thread! when I would expect the run-thread! to return immediately with the Thread object and the printlns happen in the background.

manderson19:01:34

Reason: spec/instrument attempts to validate the my-runnable lambda per my fspec, before calling the function, which starts a continuous loop and the run-thread! fn is never called!

manderson19:01:58

Lesson learned: don't instrument a "runnable" fn that doesn't terminate...

bfabry19:01:11

ah. I see. yes don't instrument functions with side-effects

manderson19:01:26

Yep. Makes perfect sense now, but took me a while to figure it out 🙂

bfabry19:01:28

yeah I totally forgot, but it has been mentioned before

manderson19:01:37

Thought I could save someone a potential headache by mentioning here...

bfabry19:01:57

there was talk of more coming for dealing with spec+side-effects maybe, but I'm not sure if it still is

manderson19:01:03

Yeah, it's one of those things you know in the back of your mind, but forget when in the thick of debugging a weird error

bbloom20:01:45

i’m not sure i understand what you say is happening here...

bbloom20:01:31

i did this:

bbloom20:01:32

(s/fdef f :args (s/and #(do (.printStackTrace (Exception.)) %) any?))

bbloom20:01:36

instrument

bbloom20:01:01

then call f or (.start (Thread. f))

bbloom20:01:07

from what i can tell, the spec-checking-fn is called by Thread.run

bbloom20:01:13

which seems right to me… sooo what’s the problem?

joshjones20:01:26

what are you trying to accomplish?

joshjones20:01:46

When I do this I get a stack trace, is this not what you want?

bbloom21:01:03

I was trying to understand @manderson and bfabry’s discussion above

manderson21:01:07

@bbloom problem is with the fspec. It's not intended for fn's with side effects, which a while true looping runnable most certainly is. When instrument is turned on, it attempts to validate the fspec by testing it out with different data to ensure it conforms. This ends up running the loop in the current thread and never actually executing the originally intended function.

manderson21:01:48

Threw me for a while, because it looked like it was running as intended, but was running in the current thread.

manderson21:01:56

As part of instrumentation

bbloom21:01:09

huh? fspec should work just fine for functions with side effects outside of generative testing

bbloom21:01:21

what “different data” is it using?

bbloom21:01:38

instrumented functions are only called once - with the same args they are given, and presumably on the same thread they would normally have been called on

gfredericks21:01:39

@bbloom instrumenting uses random data to validate functions passed as an arg to the instrumented fn

bbloom21:01:59

wait … what?

gfredericks21:01:10

e.g., if you spec clojure.core/map

gfredericks21:01:15

whose first arg is a function

gfredericks21:01:18

then you instrument map

gfredericks21:01:25

then you call it with some function

gfredericks21:01:37

that function will be test.checked as part of the arg-validation step

bbloom21:01:54

whoaaaa OK now it finally makes some sense why :fn and :ret aren’t checked by instrument

gfredericks21:01:09

yeah? I haven't connected these two things

bbloom21:01:34

i was under the impression the purpose of instrument was to make sure i wasn't calling other people’s stuff wrong

bbloom21:01:51

and that it was also used during generative testing to validate my stuff

bbloom21:01:01

i had no idea generative data was happening if i did instrument normally

gfredericks21:01:02

if somebody wrote a clickbaity listicle of facts about clojure.spec this would be the "number seven will shock you"

gfredericks21:01:43

it's not generatively tested the instrumented function

gfredericks21:01:05

it's generatively testing fspec args to the instrumented function; so for non-HOFs this doesn't apply at all

joshjones21:01:15

“number three: ALL spec’d keys in a map are validated, not only those explicitly stated as :req‘d …” 🙂

manderson21:01:21

yep, i discovered this ^^^ when i had some println's for debugging and saw a bunch junk printed out i wasn't expecting. just didn't connect it with my issue above, but it hits home the point as to why fspec isn't for side effects

manderson21:01:33

the generative testing of fspec, that is

bbloom21:01:48

i’m pretty confused now.

gfredericks21:01:04

is that the advice? don't use fspec at all for side-effecting functions?

bbloom21:01:20

ignore what i said - i still don’t understand what is happening here

bbloom21:01:35

if i use stest/instrument

bbloom21:01:41

and then call some instrumented functions from the repl

bbloom21:01:45

are generators involved at all?

bbloom21:01:49

my impression was no

bbloom21:01:01

… when? why?

gfredericks21:01:15

think about the spec for clojure.core/map

gfredericks21:01:47

(s/cat :f (s/fspec :args (s/cat :x any?) :ret any?) (s/coll-of any?))

gfredericks21:01:49

something like that

gfredericks21:01:05

imagining it only took 2 args

gfredericks21:01:22

if you instrument map, that means you're asking for its args to be validated

gfredericks21:01:37

so you instrument and then from the repl you call (map inc [1 2 3])

gfredericks21:01:57

so you must be expecting spec to validate that [1 2 3] matches (s/coll-of any?), but what do you expect it to do with inc?

gfredericks21:01:09

it could merely check that inc is an IFn

bbloom21:01:18

holy hell - that’s what i would have expected: to just check ifn

bbloom21:01:25

i expected only instrumentation of var calls

gfredericks21:01:29

number seven will shock you

bbloom21:01:36

i would not expect instrumentation of higher order functions - that’s crazy

bbloom21:01:43

clojure is just not prepared for that

bbloom21:01:00

in the absence of chaperones, proxies, and all that other crazy platform stuff that racket contracts have

bbloom21:01:09

in the presence of pervasive effects

bbloom21:01:12

that’s just nuts

bbloom21:01:50

nnnow i understand - here i am trying to recreate the problem discussed above with thread/start etc, but it was only a problem with higher order functions in there

joshjones21:01:54

well, that’s what fspec is mostly concerned with — hof

manderson21:01:11

As example:

(spec/fdef test-fn 
           :args (spec/cat :str string? 
                           :fn (spec/fspec :args (spec/cat :x string?) :ret nil?)) :ret nil?)

(clojure.spec.test/instrument)

(test-fn "hi" #(println %))

U
O

6K1Y
sqD
4PsKAL4
1wbc5
1kq9bvc
wqgAN0gb
kdKAI1aE24
22r
2
6TUvuD2x8c
95NKQmTX6c7kdTO
98O0dGrT9vr65
PE5712237F7
6
Rwc93xcj510gn7
MOvNSAJl
Received x: hi
hi
=> nil

manderson21:01:37

So, if #(println %) was instead a fn with side effects (looping, hitting a db, etc), then you can see the problem...

potetm21:01:57

Interesting. So, long story short: HOFs are "instrumented" via generative testing?

bbloom21:01:38

seems really weird to me

potetm21:01:40

I'd never considered it directly. I think deep down I was thinking they would be proxied.

manderson21:01:42

so, in my case, i was seeing my runnable fn executing, and it looked like it was functioning as normal, except it was running in the current thread instead of in a new thread.

bbloom21:01:47

yeah proxied makes more sense

potetm21:01:28

But I understand the argument for non-proxied verification.

bbloom21:01:31

ie you check it’s ifn? or fn? and then wrap it with clojure.spec.test/spec-checking-fn (private function that does the proxying)

bbloom21:01:46

this makes instrument much less useful for repl use

manderson21:01:21

yep, i went back and changed some of my specs to #(instance? IFn %)

gfredericks21:01:26

rich talked about the proxying approach in a ML thread somewhere

potetm21:01:37

TBH, I'm not sure what utility there is in specing a side-effecting function

bbloom21:01:50

before i look for that thread - the reason i wouldn’t expect proxying is b/c it breaks equality potentially

gfredericks21:01:58

I think he thought it was difficult to design well because of having to track the proxy as it flies around the program

potetm21:01:59

Yeah that's^ what I was thinking.

gfredericks21:01:00

once you use proxying, the validation of your function can happen anywhere at any time, not just at the moment of calling it

potetm21:01:06

"We do verification here and only here."

gfredericks21:01:15

e.g., with map, it would immediately return a lazy seq

gfredericks21:01:29

and then blow up later when somebody consumed it? or not blow up because the function call already ended?

potetm21:01:38

Creates separation in execution.

gfredericks21:01:47

if you unstrument before consuming the lazy seq what should it do?

bbloom21:01:51

this is all why my assumption was that instrument only instrumented vars

bbloom21:01:11

ie it was a better than nothing interactive tool

bbloom21:01:37

cover the 90% case, rather than attempt the 100% case with fallout such as “can’t use instrument if you fspec any side effecting functinos"

potetm21:01:39

Yeah the fact that it does that by default and "transparently" is... unexpected.

bfabry21:01:25

instrument does only instrument vars

bbloom21:01:45

well, i mean only calls to vars and not use the generators

bfabry21:01:21

but, instrumentation means validation of arguments to a function, if one of your argument is an fspec, validation necessarily means generative testing

potetm21:01:49

code is data, data is code bruh

bfabry21:01:50

so, don't write an fspec for functions that could potentially be side-effecting 🙂

gfredericks21:01:52

@bbloom I think I'm remembering a long thread on the clojure ml (not -dev) from 3-9 months ago

bfabry21:01:04

I suppose the other thing you could do is stub out specs for functions that are potentially side-effecting

potetm21:01:38

That seems to be the intended path^

bfabry21:01:06

hopefully the next screencast/blog on spec is advice on impure functions 🙂

potetm21:01:47

Yeah.... it's still interesting to me that they would commit to verifying HOF args. I think see the argument (pun definitely intended), but "if 'x is on classpath' do 'y' else 'z'" seems complex.

potetm21:01:40

I mean, I think it brings back up the question: "What's the relative utility of specing side-effecting fns?"

potetm21:01:37

If the answer is "so low it's not worth specing them," then committing to verifying HOFs makes a lot more sense to me.

bbloom21:01:43

it’s only low value if you view spec-ing as a testing mechanism and not as developer guard rails

potetm21:01:31

Yeah perhaps. OTOH, if you view side-effects as poison, no guard rail will save you.

bbloom21:01:43

i don’t view side effects that way tho

potetm21:01:04

Yeah, Rich has said exactly that I think (in Simple Made Easy)

bbloom21:01:07

you can say that the function passed to reduce should be effect-free, but it’s frequently useful to gather side data or whatever

potetm21:01:56

(Not saying it's right or wrong. But the decision makes sense when viewed with that mindset I think)

bbloom21:01:14

i think the decision makes sense when you consider that conform is trying to say “yup, this thing is valid"

bbloom21:01:20

vs “as far as i can tell, this thing isn’t invalid"

bbloom21:01:31

the former requires checking everything, the later doesn't

bbloom21:01:44

if you want to validate some data off the wire for security reasons, you need the former

bbloom21:01:27

but that doesn’t work in the presence of higher order functions or mutation - as the racket contracts folks learned, hence proxies, chaperones, etc - and still, they have problems with side effects b/c they don’t have an effect handlers system

bbloom21:01:04

@gfredericks I realize I’m a heretic in clojure-land, but i abuse side effects quite frequently 😛

potetm21:01:17

I've never used it that way either 🙂 I've use reductions when I'm curious what happens during reducing, but not side-accumulation.

joshjones21:01:29

I can also say I’ve only ever used the reducing function to .. well, reduce the collection at hand

joshjones21:01:12

@bbloom can you give an example of a side-effecting reducing function? (like a common use case for what you’re describing)

gfredericks21:01:11

@bbloom do you sprinkle local atoms all over the place and swap them a bunch and then deref them to figure out what happened

bbloom21:01:26

i’ve done stuff like that - yeah

bbloom21:01:58

if it’s local to a function or a concrete process, it’s totally fine

bbloom21:01:04

“tree falls in a forrest” and all that

potetm21:01:17

Anyways, not sure I understand what @bbloom was trying to say about conformers with regard to the decision to validate higher order fns, but it makes some amount of sense to me having thought about it a minute.

bbloom21:01:20

especially when perf is involved

bbloom21:01:35

using map/filter/reduce etc in 9 passes over a long sequence is just not OK for some use cases

bbloom21:01:02

i’d love to be able to tell the compiler “hey, these three things traverse the same data structure, do loop fusion” but that’s just not realistic

joshjones21:01:17

transducers in the 9 pass case! 🙂 (if applicable)

potetm21:01:28

Yeah, I'm curious how that's more useful than loop/recur or doseq+accumulator.

bbloom21:01:45

i do the loop/recur or doseq thing plenty too

potetm21:01:53

Yeah me too

bbloom21:01:54

but sometimes yo ujust already have a function, so you just call reduce 🙂

bbloom21:01:43

anyway - this is waaaay besides the point

bbloom21:01:15

point is that valid? wants to return true, but really the best it can do in the presence of fspec is say “maybe true"

bbloom21:01:33

also, i gotta run

bbloom21:01:38

thanks all for the discussion