Fork me on GitHub
#clojure-spec
<
2017-07-10
>
anticrisis05:07:02

Hiya folks, does this look like a valid spec? (s/def ::test (s/cat :k keyword? :rest (s/* any?)))

anticrisis05:07:59

The reason I ask is I get different results with s/valid? and an instrumented function

anticrisis05:07:55

I.e. (s/valid? ::test [:foo]) => true but when used in s/fdef test-fn :args (s/cat :t ::test) it fails instrumentation

seancorfield05:07:26

@anticrisis regex-style specs combine unless you wrap them with s/spec as I recall.

seancorfield05:07:16

So (s/fdef test-fn :args (s/cat :t (s/spec ::test)))

anticrisis05:07:30

there's something I have a great deal of trouble wrapping my head around with these regexp specs I think

anticrisis05:07:21

that does the trick, many thanks -- I will meditate on why, for a few hours.

seancorfield05:07:35

If you do (s/exercise (s/cat :t ::test)) you'll see what's going on...

seancorfield05:07:41

...it generates sequences and they conform to {:t {:k first-element :rest [other-elements]}}

seancorfield05:07:23

Perhaps easier to see with this example:

boot.user=> (s/def ::pair (s/cat :a keyword? :b pos-int?))
:boot.user/pair
boot.user=> (s/def ::four (s/cat :ab ::pair :cd ::pair))
:boot.user/four
boot.user=> (s/valid? ::four [:x 1 :y 2])
true
boot.user=> (s/conform ::four [:x 1 :y 2])
{:ab {:a :x, :b 1}, :cd {:a :y, :b 2}}
boot.user=>

anticrisis05:07:52

that part makes sense, but I'm not connecting it to the use of (s/spec) to change the behavior of the s/* operator when defining a function spec

anticrisis05:07:41

I'm scrutinizing the output of s/exercise with and without s/spec right now

anticrisis05:07:53

To me, the conformed output of (s/exercise (s/cat :t (s/spec ::test))) and (s/exercise (s/cat :t ::test)) look the same

anticrisis06:07:55

Thanks for tip about s/specwith regexp ops and s/exercise, they point me in the right direction, even if I don't completely understand it. Many thanks.

anticrisis06:07:33

I still think there's something inconsistent going on, either because I'm misusing this simple s/* form, or because something's broken. Here's a short walkthrough to demonstrate:

anticrisis06:07:54

(s/def ::test (s/cat :k keyword? :rest (s/* pos-int?)))

(s/conform ::test [:foo 1 2 3 4])
;; => {:k :foo, :rest [1 2 3 4]}

(s/conform (s/spec ::test) [:foo 1 2 3 4])
;; => {:k :foo, :rest [1 2 3 4]}

(s/conform (s/cat :t ::test) [:foo 1 2 3 4])
;; => {:t {:k :foo, :rest [1 2 3 4]}}

(s/conform (s/cat :t (s/spec ::test)) [:foo 1 2 3 4])
;; => :clojure.spec.alpha/invalid

(defn test-fn [test] nil)
(s/fdef test-fn :args (s/cat :t ::test) :ret nil?)

(stest/instrument `test-fn)
(test-fn [:foo 1 2 3 4])
;; => exception

(s/fdef test-fn :args (s/cat :t (s/spec ::test)) :ret nil?)

(stest/instrument `test-fn)
(test-fn [:foo 1 2 3 4])
;; => nil

;; Note that in order to get test-fn to pass instrumentation,
;; its :args must use the same form which conform considers invalid

seancorfield06:07:20

(s/conform (s/cat :t (s/spec ::test)) [[:foo 1 2 3 4]])
One element, of type ::test.

seancorfield06:07:38

similarly your function accepts one argument, of type ::test

seancorfield06:07:21

The :args in s/fdef is the sequence of arguments.

seancorfield06:07:31

For your first s/fdef -- :args (s/cat :t ::test) -- your test-fn would have to be (defn test-fn [kw & nums] nil) -- first argument :k, a keyword, and the :rest of the arguments are numbers.

anticrisis06:07:03

I see what you're saying... but I thought :args (s/cat :x ::whatever) meant "the arguments are a list of one element, labeled 'x', which can be conformed with spec '::whatever'

anticrisis06:07:24

Your example of fdef'ing [kw & nums] is helpful to tell me I've been thinking about this all wrong. I would have fdef'd it like this: :args (s/cat :k keyword? :nums (s/* pos-int?)) -- which you're telling me is the same as :args ::test-- which it is, but those particular neurons in my brain apparently weren't connected.

anticrisis07:07:22

For the record, I'd like to point out that nesting data structures is confusing. Is there a version of clojure I can use that doesn't do that? Thanks. 🙂

seancorfield07:07:48

If you want actual sequences (and nested sequences) then use coll-of etc, not cat 😸

hkjels07:07:23

How can I stub a function? Typically I’d like to require that on-click is a function, but it fails when I later exercise it since theres no generator for it

hkjels07:07:29

Creating a custom generator would be overkill I think

gfredericks11:07:20

@hkjels would a custom generator of (gen/return (constantly nil)) be overkill?

rickmoynihan13:07:39

Hmmm… I’ve just made an observation about specs which I’m wondering if others could validate for me… Basically I have a layered architecture where we do something like this: (->> input transform-1 transform-2 transform-3) and I’ve just realised that in layout terms it seems better to organise the input specs alongside the transforms, rather than the :ret specs. It also seems to more closely align with things like instrument checking :args rather than :rets Has anyone else noticed this?

rickmoynihan13:07:57

What is the most idiomatic way to spec something to be a single value? I’m guessing: (s/def ::single-value #{:foo})

mpenet13:07:00

yep, you get free gen with that one

rickmoynihan14:07:41

mpenet: yeah the free gen is a big motivator 🙂

mpenet13:07:26

in theory (constantly :foo) would work for validation but no gen

gfredericks14:07:03

(constantly :foo) would work? That seems identical to any? to me

gfredericks14:07:19

it's a predicate that always returns truthy

mpenet14:07:33

bad example

gfredericks14:07:43

#(= % :foo) I guess?

mpenet14:07:51

yes or #(identical? :foo %)

gfredericks14:07:07

just for keywords

mpenet14:07:40

but anyway, the set solutions is better in most case

gfredericks14:07:20

I can imagine people stumbling on #{nil} relatively often

mpenet14:07:19

well I guess you want nil? in that case

gfredericks14:07:55

I guess that sort of error isn't likely to go unnoticed

ghadi17:07:58

Bumping that question -- I was looking for some prior art for custom printing mostly

bbrinck18:07:45

@ghadi I’m working on something now, but it’s still pre-alpha

bbrinck18:07:48

hope to have something ready by thursday 🙂

bbrinck18:07:12

The code is under change right now, but a preview of what I’m trying to achieve is in the tests e.g. https://github.com/bhb/expound/blob/master/test/expound/spec_test.cljs#L120-L137

ghadi19:07:33

bbrinck: very cool

bbrinck18:07:35

Excuse the lack of documentation or sane organization

bbrinck18:07:49

Has anyone found a use case for specing a map with coll-of? I see that you can create a spec like (s/def :foo/map (s/coll-of int? :kind map? :into {})) but I can’t see why this would be useful since everything but the empty map would always fail.

bbrinck18:07:18

I suppose you could maybe replace int? in that example with a spec for the key/value pairs, but in that case, map-of seems superior.

Alex Miller (Clojure team)18:07:41

there are some cases where it is useful to spec a map as a sequence of entry tuples

Alex Miller (Clojure team)18:07:17

“hybrid” maps (combination of s/keys and s/map-of styles) is one case

souenzzo20:07:36

alexmiller: When I look to the result of (s/conform ::binding-form it remember me about ast's ... may in the future, can spec/conform be used as analyzer?

Alex Miller (Clojure team)20:07:30

If you use that in the general sense, then yes absolutely - it’s a great tool for turning s-expressions into a tree of maps describing a DSL syntax

bbrinck18:07:05

@alexmiller Ah, I see. You’re using a tuple as the “inner” spec. That makes sense. Thanks for the info!

Alex Miller (Clojure team)18:07:27

that’s actually how s/map-of and s/every-kv are implemented

bbrinck18:07:35

Interesting. Your example was instructive. I can see how oring together several tuples would let me define different valid key/value pairs

bbrinck18:07:42

Basically, it lets me create a clear relationship between the spec for certain keys and the specs for their values. But combine them all into one big map spec

bbrinck18:07:37

“valid key/value pairs” Sorry, I meant “valid key/value pairs of specs

lfn320:07:14

Is fspec meant to handle cases where the provided function throws? It’s causing spec errors when instrumented.

lfn320:07:23

I’ve got a relatively minimal repro:

(t/use-fixtures :once #(do (s.test/instrument)
                           (%1)
                           (s.test/unstrument)))

(defn takes-fn [f]
  (try (f true)
       (catch Error e
         false)))

(s/fdef takes-fn
  :args (s/cat :f (s/fspec :args (s/cat :bool boolean?)
                           :ret boolean?))
  :ret boolean?)

(deftest can-handle-throws
  (is (false? (takes-fn #(if %1
                           (throw (ex-info "scary exception!" {}))
                           true)))))

lfn320:07:12

That yields: In: [0] val: (true) fails at: [:args :f] predicate: (apply fn), scary exception! when the test is run

Alex Miller (Clojure team)20:07:04

when you have an fdef with an fspec arg, the instrumented var will check the fspec by generating from its args spec and invoking the function to validate it conforms to the spec

Alex Miller (Clojure team)20:07:27

in this case it will generate boolean values and invoke f

Alex Miller (Clojure team)20:07:41

specs cover non-exceptional use (as those are the exceptions :) so I think a more accurate fspec args spec in this case would use false?, not boolean?. it would then invoke f with only false and not trigger the exception

lfn321:07:58

Right I guess I just wasn't expecting the fspec'd arg to be invoked? Wouldn't it be better to use with-gen so the spec still captures the allowed range of values?

Alex Miller (Clojure team)22:07:20

well the idea is that you said this function takes a function that responds a certain way - the only way to validate that is to invoke it

lfn322:07:04

Yeah agreed. I don’t think that was really a question, I was just kinda surprised. I would have assumed that the fn would be instrumented and validated when it was invoked rather than when it was passed into another function. Might be something worth mentioning in the docs, if it isn’t already?

Alex Miller (Clojure team)22:07:27

This has been brought up several times but I haven't had a chance to discuss it with Rich

lfn323:07:05

Ok. Thanks for the clarification and explanation in any case, it’s much appreciated.