Fork me on GitHub
#clojure-spec
<
2019-02-04
>
daniel-tcgplayer15:02:34

Hey everyone, I'm experiencing something odd trying to implement spec into our project for the first time. I'm using fdef to define my function's spec, however it's not validating my arguments. I'm using clojure 1.10. Here's the code:

(defn inc-num [x]
  (inc x))

(s/fdef inc-num
  :args (s/cat :x number?)
  :ret number?)
Calling this function (inc-num "a") throws a ClassCastException instead of the spec error. Any ideas?

taylor15:02:58

did you s/instrument the function too?

taylor15:02:10

that's what enables the :args checking at runtime

daniel-tcgplayer15:02:05

I did not! Let me go ahead and do this, maybe I got ahead of myself in the documentation 🙂

borkdude15:02:55

yes, writing an fdef does not turn on instrumentation automatically. also note when you re-define the fdef, you have to call instrument again to make it effective

daniel-tcgplayer15:02:58

I see. So for all functions that I'm relaying on fdef to spec, I must also instrument those functions

borkdude15:02:24

you can instrument all function at once by calling (stest/instrument)

borkdude15:02:49

if you’re using component, you might want to hook it up to the start/stop cycle. no, globally

taylor15:02:41

yep that'll instrument everything that's been loaded, across all namespaces. there's also unstrument to do the opposite

daniel-tcgplayer15:02:33

Awesome! Thanks everyone for the quick feedback, I'm movin' again. I'll go through the documentation more thoroughly

taylor15:02:28

@U6TUZTAAF I wrote some more function spec examples here too: https://blog.taylorwood.io/2017/10/15/fspec.html

🔥 1
💯 1
borkdude15:02:16

I’ve never used fspec. does spec really check the arity of such a function when you call a higher order function whose function argument is spec’ed with it?

taylor15:02:05

@U04V15CAJ fdef uses fspec internally, so there's not much difference AFAIK. when you spec a function that takes another function as an argument, spec invokes the passed function ~20 times — a kind of mini-check — to see if it conforms

borkdude15:02:35

spec invokes then function when?

taylor15:02:49

whenever you call the higher-order function, if it's instrumented

borkdude15:02:12

it doesn’t just call it when it’s called normally?

taylor15:02:00

not sure I understand the question

taylor15:02:19

(defn f [g] (g 1))
(s/fdef f :args (s/cat :g (s/fspec :args (s/tuple int?))))
(st/instrument `f)
(f #(doto % prn inc))
-1
0
-2
...
59
-69770
-1165
1
=> 1

taylor15:02:07

so whenever you call f (instrumented), spec is going to do a lil mini-check of the function you pass to f to see if it conforms to the fspec, and if it does then of course f will call g again

borkdude15:02:47

why is that sane behavior? I can see this being useful when doing generative testing

borkdude15:02:06

wut…

$ clj
Clojure 1.10.0
user=> (require '[clojure.spec.alpha :as s])
nil
user=> (require '[clojure.spec.test.alpha :as stest])
nil
user=> (s/fdef clojure.core/keep :args (s/cat :f ::keep-fn :coll seqable?))
clojure.core/keep
user=> (s/def ::keep-fn (s/fspec :args (s/cat :x any?) :ret any?))
:user/keep-fn
user=> (stest/instrument `keep)
[clojure.core/keep]
user=> (keep inc [1 2 3])
Execution error (FileNotFoundException) at user/eval144 (REPL:1).
Could not locate clojure/test/check/generators__init.class, clojure/test/check/generators.clj or clojure/test/check/generators.cljc on classpath.
user=>

taylor15:02:12

I think you need test.check on your classpath

borkdude15:02:55

yeah, I know why this happens, I’m just surprised that it suddenly wants to do generative testing while it can just conform while running, like normal functions

taylor15:02:20

there's no way to check an fspec without invoking the function though

borkdude15:02:00

oh, now I see, because they are not replaced by an instrumented version… right.

borkdude15:02:14

but still, it’s a little weird this.

borkdude15:02:01

you would have to be very wary of using side effects in fspec’ed functions

☝️ 1
borkdude15:02:05

I haven’t used fspecs in speculative yet and this may be a reason I’m not going to..

taylor15:02:20

I think the problem is more about speccing higher-order functions in general, than it is purely about fspec, b/c fdef is just a shorthand for s/def + s/fspec. another option is to just use fn? or ifn? when you're speccing HOFs, that way spec doesn't mini-check them

borkdude15:02:39

yes, that’s what we’re doing now

borkdude15:02:00

in most cases it’s sufficient, although it would maybe nice to be able to speak about function arity

taylor15:02:16

one case where I can see value in fspecing args to HOFs is when you check the HOF, spec will pass it a dummy function that expects the right :args and returns values generated according to the :ret spec

borkdude15:02:34

that would be useful, but I want this to be pluggable. so my fdef would not use fspecs, but I do want to plug them instead of ifn? during generative testing.

borkdude15:02:18

you can do that by overriding spec generators

👍 1
borkdude15:02:14

but overriding ifn? would be a bit too global, since the args and ret could use ifn? with different properties

borkdude15:02:29

so then you would have to give the argument list a named spec

borkdude15:02:36

which I what I do sometimes

drone14:02:09

chiming in that the generator requirement for HOFs also does not seem sane to me. should be able to instrument without assumption that you’re using generative testing

borkdude14:02:44

I’d say so too. @alexmiller maybe should be an option to turn this off on spec2?

alexmiller15:02:58

it’s a topic we’ll revisit

✔️ 1
hmaurer20:02:32

Is there a way quick way, without using an external lib, to generate a random value from a spec while limiting the max depth when the value is a map?

hmaurer20:02:00

e.g. I have nested s/keys specs and I would like to generate a random value for the spec but limit the depth of the data structure

hmaurer20:02:20

I know there are some libs on github that do this, but I am wondering if there is a short way to do it out of the box with specs

taylor20:02:28

is it a recursive spec? if so, there's a *recursion-limit* binding you can use during generation. the other thing that comes to mind is passing in a small/fixed size arg to the sample function

hmaurer20:02:28

@taylor yes it’s recursive, although not directly. The spec is modelling an AST

hmaurer20:02:54

@taylor I’ll try that, ty! Also I am trying to generate random values for this spec:

(s/def :elogic/term (s/and (s/keys :req [:elogic.term/type])
                           (s/multi-spec elogic-term-type :elogic.term/type)))
but I am getting “Couldn’t satisfy such-that predicate after 100 tries.”

hmaurer20:02:58

any idea how I can make it work?

taylor20:02:03

yeah that's b/c the generator for your s/and is only based on the first inner s/keys spec, and the generated values from that don't conform to the second multi-spec

hmaurer20:02:53

@taylor ah. Any way around this?

taylor20:02:57

you can wrap that s/and with s/with-gen to specify a custom generator, or override the generator elsewhere

taylor20:02:17

some of the spec functions take an overrides map for this

hmaurer21:02:07

is there a way to define a spec on a string without seq’ing it first?

hmaurer21:02:08

a regex spec

hmaurer21:02:32

@alexmiller is it a generally bad idea to use specs to parse strings?