Fork me on GitHub
#clojure-spec
<
2019-08-13
>
murtaza5205:08:37

I have a sorted collection, I want to define the relationship between 2 elements of the collection ie the first element is greater than the second element. Is this possible ?

seancorfield05:08:56

@murtaza52 (sort comparator coll) lets you pass in a comparator that defines the relationship on which you are sorting.

seancorfield05:08:10

user=> (sort (reify java.util.Comparator (compare [_ a b] (cond (even? a) -1 (even? b) 1 :else 0))) (range 20))
(18 16 14 12 10 8 6 4 2 0 1 3 5 7 9 11 13 15 17 19)
user=> 

seancorfield05:08:44

(a silly example, but it sorts all even numbers ahead of all odd numbers 🙂 )

seancorfield05:08:50

Is that an answer to the question you were asking? (I wasn't entirely sure what you were asking)

murtaza5205:08:06

@seancorfield thanks for the example 🙂. However , I am already using a custom comparator for my sorting, however how do I use that in my spec definition ?

seancorfield05:08:36

I guess I don't understand what you're trying to do...

murtaza5205:08:36

I have a function that does the sorting. Now I want to spec the fn, to specify that the output collection is sorted

Alex Miller (Clojure team)12:08:30

We are kicking around some ideas for stating this constraint in spec 2

seancorfield05:08:16

This is one of those cases where the spec would almost need to be the implementation. I think that's over-specification to be honest.

seancorfield05:08:41

I very rarely specify the result to that level of detail.

seancorfield05:08:35

After all, it's not like instrumentation checks :ret or :fn so this is really only about generative testing and I think that can be better served by property-based testing, not direct :ret/`:fn` spec testing.

seancorfield05:08:50

For example, for any collection with count > 1, good properties to test are that the comparator produces the expected result for the first and second items, and for the first and last items.

seancorfield05:08:57

It's tempting to "spec everything" when you get started but it's really not very productive in my experience.

murtaza5205:08:38

ack makes sense from a testing perspective. however what I like about specs is also the documentation it produces. So when I spec a relationship, it also helps other devs to know what the fn is going to return.

seancorfield05:08:45

Spec should be "just enough" to sanity check what you're writing -- unless you are specifically using it to validate input data (or output data), rather than specific functions.

seancorfield05:08:16

@murtaza52 I'm not sure that using Spec to that level of detail, just for documentation, is productive. I would say "most functions do not need to be Spec'd" but you could describe the behavior in the docstring without adding the overhead of Spec...

seancorfield05:08:04

Spec makes the most sense on boundaries and for the "critical" functions. Spec'ing everything makes code brittle.

murtaza5205:08:44

@seancorfield thanks for the insight

seancorfield06:08:39

In my mind, this is why type systems can make code brittle -- you end up with types everywhere rather than just where they "make sense". It's one of the things I really like about Clojure: you can write a lot of generic code and a lot of abstractions without introducing that brittleness -- but you also have Spec for defining data structures and for certain functions on the edges of modules.

murtaza5206:08:28

yup makes sense

murtaza5210:08:34

@seancorfield so trying to think through, in what situations does spec’ing a fn makes sense, so that it could instrumented later ? Bcoz as u mentioned spec’ing all fns dont make sense. The ones on the boundary I am already validating. So where does the optional spec’ing come in ?

seancorfield16:08:12

Spec'ing functions can be useful for development / testing (instrumentation / generative checking) but I would generally only spec functions that are part of module APIs or "important" functions. There's no hard and fast rules here -- just spec what is "useful" to spec. For example, in next.jdbc, there are optional instrumentation specs for the public API functions -- for users of the library -- and those are instrumented during test runs too, for me as the developer of the library (but it's also important run the tests from time to time without instrumentation to see what behavior/errors crop up in known bad input data).

seancorfield16:08:56

I think you just have to find what works for you in terms of the amount of checking you need during development/testing and in terms of what you want to provide for clients of your code.

seancorfield16:08:30

It's a bit like the question "Which functions should I unit test?" -- the answer is "Not all of them" but there are no hard and fast rules there either.

murtaza5206:08:00

In the below spec -

(s/def ::id (s/and string? #(not (clojure.string/blank? %))))

  (s/def ::my-event (s/keys :req [::id]))
this is valid - (s/explain ::my-event {::id "a"}) this is invalid - (s/explain ::my-event {:id "a"}) So do all my keys in the input have to be namespaced ? Because I am not able to validate data when the keys are not namespaced.

seancorfield06:08:46

If you use :req then, yes, the keys must be namespaced. If you use :req-un then you have have simple keywords.

seancorfield06:08:42

With :req-un, you can have ::foo/bar as the spec for the key -- but the key will be :bar. That allows you to have different specs for the same key name in different contexts.

murtaza5206:08:26

thanks, I was confused bcoz when I used gen, it generated data without namespaced keywords. So my assumption was that it will also me to validate ones without ns.

murtaza5206:08:52

So moving ahead is it more idiomatic to have namespaced keys in data too ?

schmee07:08:28

I have run into what I find to be a tricky modeling problem with spec. I have data that looks like this (excluding namespaces on the kws for brevity)

:event-type :state-transition
:old-state :running
:new-state :exception
Those three keys are always included, but if the event type is :state-transition and the new state is :exception, then there will be an :exception key as well (there are a couple of more special cases like this). What I would like is some form of “wildcard”, so that I can dispatch like so:
defmethod foo [:state-transition :exception]
defmethod foo [:state-transition _] <-- wildcard for the default case
Without the wildcard, I need a defmethod for all possible combinations state-transition/new-state which will lead to a lot of duplication. Is there a better way to accomplish this, in either spec1 or spec2?

jaihindhreddy08:08:59

Name the special cases and use s/multi-spec with a dispatch fn that returns these names, then impl them by extending the multimethod.

murtaza5212:08:41

Is there a pred for specifying only 1 param in a collection. I find this pattern when I want to spec the input args of a fn. Lets say if the fn expects a hash-map, I usually define the spec for args as :args (s/? ::map-spec). This pattern works, however is not correct bcoz it allows no values too.

Alex Miller (Clojure team)12:08:20

s/cat is usually the best top level spec for args

Alex Miller (Clojure team)12:08:15

You can match up the tags and the args too

murtaza5212:08:44

thanks, yup that is better

Alex Miller (Clojure team)13:08:42

if you haven't seen the guide, it has several examples https://clojure.org/guides/spec

murtaza5213:08:12

yup have gone through it, and its open on machine as a reference, however sometimes tend to miss a detail.

murtaza5213:08:59

what is a good way to generate date time, I have a spec - (s/def ::start inst?), however the dates that it generates are very similar, and most are the same. (gen/sample (s/gen ::ks/start) 5) =>

(#inst "1969-12-31T23:59:59.999-00:00"
 #inst "1969-12-31T23:59:59.999-00:00"
 #inst "1969-12-31T23:59:59.999-00:00"
 #inst "1970-01-01T00:00:00.001-00:00"
 #inst "1970-01-01T00:00:00.000-00:00")

Alex Miller (Clojure team)13:08:58

inst-in will generate only dates near the start range in the first few samples but will then range farther

Joe Lane13:08:10

Is that why sometimes I see a pattern to (drop 1000 ...) with some of the generators?

ghadi13:08:56

my general pattern for this stuff is to generate a bunch of deltas, and add them to a constant "anchor" value, in this case a date

ghadi13:08:05

(But since inst-in exists, use it)

Alex Miller (Clojure team)13:08:27

which is exactly how inst-in generator works but it "grows" from the start date

Alex Miller (Clojure team)13:08:46

so test.check "shrinking" will shrink towards the start date

Joe Lane13:08:38

Is that a property of the Rose tree or is it designed explicitly for that?

murtaza5213:08:38

changed it to inst-in and this worked (drop 990 (gen/sample (s/gen ::ks/start) 1000))

Alex Miller (Clojure team)13:08:55

the rose tree is designed to shrink from greater to lesser "size" and inst-in generator builds from start date + int generator (which has its own "size")

Alex Miller (Clojure team)13:08:43

whether or not this is good is a separate question :)

Joe Lane14:08:31

hahah cool, thanks for the insight alex. It's great to read your insideclojure blog, always a joy to see what you're working on.

murtaza5215:08:09

is there a way to merge distinct s/every-kv like s/keys can be done ?

murtaza5218:08:45

I have speced a few fns with :args and :ret, however it throws an error only when the :args does not conform, no error is thrown when the :ret spec does not conform. Any ideas what I am missing ?

seancorfield18:08:52

@murtaza52 Per our thread discussion earlier: instrument checks :args but you need generative checking for :ret (and :fn).

seancorfield18:08:25

Generative checking is about testing whether the function has the expected behavior (given generated conforming arguments).

seancorfield18:08:45

Instrumentation is about checking that other code calls this function correctly during development/testing.