Fork me on GitHub
#clojure-spec
<
2016-12-23
>
sophiago02:12:57

hi. i took about a month off working on this library i was specing out and now figured it's a good time to come around and finish it since i'm going to be refactoring a bunch of things and would rather not have to end up manually debugging them. the last thing i was stuck on was defining specs for custom types from defrecords. i was trying something like this:

(s/spec ::rational? #(= Rational (type % )))
but that's obviously not correct...

seancorfield03:12:58

I.e., (s/spec ::rational #(instance? % Rational))

seancorfield03:12:43

I don't think it's idiomatic to use ? on a spec name (just on a predicate).

seancorfield03:12:06

? usually implies a Boolean result.

seancorfield03:12:39

(sorry for briefness - on my phone) ^ @sophiago

sophiago03:12:33

thanks @seancorfield! i'll give it a try

sophiago03:12:56

@seancorfield that's not compiling:

No value supplied for key: (fn* [p1__302#] (instance? p1__302# Rational))

sophiago03:12:58

it seems you had the arguments to instance reversed, but i get the same stack trace regardless

sophiago04:12:19

to provide some background on what i'm trying to test here: i have five binary functions defined in protocols over four types and want to enumerate tests on all possible combinations of types as parameters. i figure i'll just focus on getting that right for now, but the the tricky part is i then need to include certain subtyping relations between them (shouldn't be super complicated) and also make sure i have tests where the results are of each possible type to check the coercion function is working (a bit trickier). i think i should be able to build up the subtypes manually once i can figure out how to define basic ones based on defrecords, seeing as they obey certain numerical laws. and then i think i should just be able to write fdefs for each of the five functions where i provide them as a list of inputs to each parameter and then maybe use a cond to determine what the possible return values are given each pairing. the final part i suppose is making sure exercise generates a certain number of tests for each result, which i haven't really thought about yet. hope that makes sense

joshjones04:12:27

fwiw regarding the previous part of the conversation, there's already a predicate function called rational? so no need to spec it, it's already there

sophiago04:12:37

but does that pertain to Java ratios? i'm referring to a record called Rational i defined myself

joshjones04:12:04

ah, i see .. nvmd 🙂

joshjones04:12:55

i am guessing you've explored multi-spec and that it's not applicable for your use case?

sophiago04:12:58

thanks, tho...i didn't know they supported that. last i heard there was just int? and float? so seems odd to have rational? given the level of specificity and little use that type gets

sophiago04:12:13

no, i haven't looked at multi-spec

joshjones04:12:51

there are several new predicates in 1.9, such as nat-int?, pos-int? .. all very good set of built-in "specs"

joshjones04:12:45

the great thing about spec is that any predicate is a spec, so clojure already has tons of them built right in

joshjones04:12:29

I have not had a need for multi-spec yet but it is related to spec-ing based on types; I suggest looking at that section in the spec guide and perhaps it may be of some use for your use case.

sophiago04:12:49

yeah actually i am familiar with muli-spec and it may make sense when i get to enumerating the pairings rather than using fdefs, but don't see how it would help with just testing for types

joshjones04:12:26

oh well, it was worth a shot 🙂

sophiago04:12:06

the guide says it's for defining things like types within spec, which makes sense, but as far as just speccing out an existing type the suggestion to use instance? seems like it should work...

joshjones04:12:35

(s/def ::mystring #(instance? String %))
(s/valid? ::mystring "asdf")

joshjones04:12:37

works for me?

sophiago04:12:51

yes, it should be exactly like that

sophiago04:12:13

except that String is a built-in Clojure type

seancorfield04:12:14

boot.user=> (defrecord Rational [a b])
boot.user.Rational
boot.user=> (require '[clojure.spec :as s])
nil
boot.user=> (s/def ::rational #(instance? Rational %))
:boot.user/rational
boot.user=> (s/conform ::rational (->Rational 1 2))
<#C053K90BR|boot>.user.Rational{:a 1, :b 2}
boot.user=>

seancorfield04:12:01

And

boot.user=> (s/explain ::rational (/ 1 2))
val: 1/2 fails spec: :boot.user/rational predicate: (instance? boot.user.Rational %)
nil
boot.user=>
as expected.

sophiago04:12:27

ah! so sorry. i just realized i was using spec instead of def

sophiago04:12:05

as mentioned...it's been a month

sophiago04:12:29

ok, hoping this gives me enough to work with until i come back tomorrow with another level of broken 🙂

sophiago04:12:37

i.e. from here it should be trivial to define subtypes using selectors already have. not sure about matching return values in my fdefs tho...

wei05:12:31

is there a way to use spec that works with both maps and datomic's entitymaps?

bbloom05:12:32

there may be something better, but you can use associative?

bbloom05:12:35

note that will also find vectors

bbloom05:12:13

so maybe #(and (not (vector? %)) (associative? %)) is passable, but hacky

seancorfield08:12:13

My initial reaction is you're way-overspecifying things... maybe...

seancorfield08:12:10

One simplification would be to have a predicate for int or Rational and use that for the numer/denom and real-part/imag-part -- that would cut a lot out of it.

sophiago08:12:21

@seancorfield yeah, that's one way to think about it. like maybe spec just isn't meant to test for this level of correctness? like whether my coercion functions are reducing values correctly based on the composition of their inputs and outputs. i just wasn't sure if there was a way to automate some of the pairing? otherwise i'm afraid i could be left with something that's not testing that much...

sophiago08:12:25

for example, i currently have known bugs with subtyping. hence why i want tests for those. that's easy on its own. but the other issue is correctness of coercion up and down the numerical tower and i'm not sure there's any way to test that (that actually makes sense...that is) since it depends on the actual values and calls to gcd

sophiago09:12:07

@seancorfield i guess on the one hand, i'm not quite sure what you mean by how i could simply it by using predicates for int and Rational? and otoh, defining the possible types was rather easy even if it does seem like overkill... where i'm stuck is if there's a way to at least partially automate testing type coercion by matching input values to return types. here's the last version of the full code if it helps explain what i'm talking about: https://github.com/Sophia-Gold/Symbolic-Algebra.clj/blob/master/src/symbolic_algebra/core.clj

sophiago09:12:02

oh wait, Sean...you're the one who wrote that old numeric tower library! funny i'm asking you about this then. i've looked at that code a bunch. this started out as intending to be a Scheme-style numeric tower, but obviously with the emphasis on types rather than functions

Yehonathan Sharvit13:12:11

What is the idiomatic way to define a spec for a collection of numbers not including Double/NaN?

alexmiller13:12:11

double-in can be used to spec doubles not including NaN - that can be combined with others

alexmiller13:12:13

Or spec something generic and add a predicate to exclude NaN (but use Double.isNaN, not anything = based)

Yehonathan Sharvit13:12:03

currently I have (s/def ::coll-of-numbers (s/coll-of number?)

Yehonathan Sharvit13:12:40

So your suggestion is to create a predicate (defn not-nan-number? [x] (and (number? x) (not (Double.isNaN x)))?

Yehonathan Sharvit13:12:57

BTW the function I want to spec is a function that receives a collection of numbers and calculates their average

Yehonathan Sharvit13:12:55

You probably meant this: (s/def ::seq-of-numbers (s/coll-of ::not-nan-number))

Yehonathan Sharvit13:12:09

(s/def ::not-nan-number (s/and number? #(not (Double/isNaN %))))

alexmiller13:12:15

Actually I would use s/and to make a spec, not a pred

alexmiller13:12:40

Well, you could do it either way

alexmiller13:12:50

There are tradeoffs

alexmiller14:12:10

Depends whether you care about gen too

Yehonathan Sharvit14:12:30

do you know if there is a isNaN that is cljs compatible?

Yehonathan Sharvit14:12:05

othrewise I would need to make my own #?(:clj Double/isNaN :cljs js/isNaN)

bhagany15:12:12

@viebel in cljs, double-in takes a :NaN? parameter… I’m kind of surprised this wasn’t in clojure first? (I’ve only done spec in cljs)

bhagany15:12:18

so, looks like you can (s/double-in :NaN? false) and get correct check and gen behavior in both clj and cljs

bhagany16:12:42

Didn’t have time to complete this thought earlier - I’d probably end up with something like (s/or :int int? :double (s/double-in :NaN? false)). This way also has the advantage of easily including ratios, if you wanted to do that. (`number?` won’t gen ratios)

sophiago18:12:41

going to try this again during the daylight...and hopefully before too many people have left for the weekend, if not already. i'm looking for some advice speccing out this project:

sophiago18:12:20

essentially i'm just wondering if there's any way to automate pairing return types based on input values like this or whether i'm just being far too ambitious about what spec can do

sophiago18:12:10

it was also suggested i might be able to simplify how i've specified all the combinations of types, but i'm not quite sure how to go about that

sophiago18:12:47

i suppose one way would be to use gen with the type constructors instead of using the selectors to define all these types? then feeding those into the input and of the functions i'm using fdef with? i suppose when it comes to checking cases where it should reduce i may have to settle for just enumerating a large number of tests i can go over visually rather than having it flag the return values as invalid for me, although that would be ideal

assoc-in19:12:42

Does anyone know if when I instrument a function with a spec that defines :ret as int? and the function I am testing returns a string if the spec error should show up in the repl? I was able to get the error of the return not being an int? when I used spec/check but expected it with instrument as well. Thanks!

assoc-in19:12:55

Also I am able to get the :arg spec error to show up with invalid inputs so I believe I am instrumenting the function correctly.

seancorfield19:12:56

instrument only verifies that functions are passed valid arguments, i.e., that the calling code “works”. If you want to verify a function “works” — testing the return and invariants — you need to look at clojure.spec.test/check.

seancorfield19:12:12

Two different types of testing.

assoc-in19:12:08

Interesting I figured it would be a nice addition to the development workflow being able to check the return types were correct on the fly like the arguments are checked

sophiago20:12:39

@seancorfield i was confused by your comment on my snippet last night. were you suggesting i use s/valid with the type constructors rather than specifying so many s/defs using the selectors and instance?

seancorfield20:12:03

I was suggesting reducing the number of specs so that you didn’t have separate cases for int + int / complex + int / int + complex / etc — I think you’re over-specifying things and making life harder for yourself.

seancorfield20:12:27

@assoc-in Probably a good idea to watch some of the videos online about clojure.spec so you understand the drivers behind its design — since expecting instrument to run :ret and :fn checks is common when folks first start using spec unless they’ve read / watched a bunch of the design justifications.

seancorfield20:12:12

@assoc-in Not sure if you’re in the #beginners channel? There’s some discussion right now about spec and someone just linked to a great talk by Stu Halloway on it.

sophiago20:12:17

@seancorfield what i'm really concerned with testing are subtyping + coercion. so i do need those cases specified for it to be useful and at least it's something i can do with relative ease. the latter...i'm less sure about. it may come down to just visually checking a large search space to see if any aren't reduced or if they cause the coercion functions themselves to throw errors. but at least for that i need a variety of subtype relationships

seancorfield20:12:21

Ah, I see… Then I don’t really have any suggestions...

sophiago20:12:58

ah ok. thank anyway. i'm just going to start by having it enumerate a certain number of each type per each function i'm testing with no :fn key or more particular restrictions on :ret

sophiago20:12:54

for the future...if i do want to use a helper function for :fn do you know the syntax for passing the arg values to that? as opposed to what i tried in that snippet?

sophiago20:12:08

or ret rather

sophiago20:12:09

i was trying to do something like this : :ret (coerce-types #(:args :a %) #(:args :b %)))

hlship23:12:05

Is it allowed to instrument protocol methods? That is, I define a protocol, and want to instrument the created dispatch function.

hlship23:12:43

So far, this is not working; I’d imagine that it would instrument the function to verify arguments & etc., then delegate to the original function to actually dispatch on type to an instance of the protocol.

hlship23:12:16

Never mind, it works. Problem w/ a test.

sophiago23:12:12

@hlship i was going to say...that's exactly what i'm working on right now!

hlship23:12:03

It’s odd; I get the expected failures from the REPL, but inside tests, I can pass non-confirming values without failure.

hlship23:12:19

I suspect this would work if it was a normal function, but Protocol methods are their own beasts.

sophiago23:12:08

hmm...that's odd

sophiago23:12:56

this is my first go at fully testing a project and i'm having trouble getting the syntax correct it seems

hlship23:12:24

I can imagine any number of load-order or bytecode generating things that could be getting in the way.

hlship23:12:44

I’d hate to have to add a function in front of the method, just to get the desired test-time spec help.

sophiago23:12:01

yeah, i'm not planning on doing that either