Fork me on GitHub
#clojure-spec
<
2018-03-22
>
misha07:03:08

@hkjels just (s/def ::hiccup (s/tuple #{:div :span} string?))?

andy.fingerhut07:03:19

So Stuart Halloway gave a talk on Clojure spec at Strange Loop 2016 that I am transcribing now (mostly done), where he said this: 'So one of the things that is fun about developing with spec is: as soon as you have made a spec, and it can even be a very rudimentary one, you can then say "give me example data" or "give me example functions that do this".'

👍 4
andy.fingerhut07:03:40

The part that I am wondering how to do is the part where he says "give me example functions that do this". Does anyone know if that is possible, and if so, has examples of doing that?

Alex Miller (Clojure team)12:03:15

He was probably referring to the stub capabilities of instrument

gfredericks11:03:12

fspecs can be generated I think

taylor11:03:24

(def my-fn (-> (s/fspec :args (s/cat :x int?) :ret string?)
               (s/gen)
               (gen/sample 1)
               (first)))
(my-fn 1)
=> "8VRbFJ59e2K2Ic79F8CX8HB1"
(my-fn 1)
=> "WSKN70JT5kZG"

taylor11:03:33

is it just making a function that ignores its inputs and using the :ret spec to generate return values?

taylor11:03:34

> fspecs can generate functions that validate the arguments and fabricate a return value compliant with the :ret spec, ignoring the :fn spec if present.

guy11:03:26

Well you could try doing (my-fn "string") and see if it complains

guy11:03:48

Because you are giving it an int right?

taylor11:03:54

it does validate the inputs, I asked before I read the docstring 🙂

guy11:03:13

:thumbsup:

gfredericks11:03:09

yes the return is unrelated to the input I think

gfredericks11:03:28

I made a ticket many decades ago to discuss whether it should at least be a pure function

gfredericks11:03:38

it's also currently independent of the test.check seed I think

guy12:03:59

You can use orchestra to make it related with the :fn i think?

taylor12:03:18

I think that only applies to instrumentation

👍 4
danielcompton12:03:55

I think this is a really dumb question, but how can I detect which branches were taken/captured in a s/cat?

gfredericks12:03:34

doesn't the conformed value tell you that?

danielcompton12:03:36

yeah I can see it in the data, I was just wondering if there was a function you could call to get back which parts matched

danielcompton12:03:12

the context is that I'm trying to parse defn forms and reassemble them with tracing wrapped around parts of the function

danielcompton12:03:55

and the function specs have quite nested data that is a little bit annoying to pull out

danielcompton13:03:57

So I end up with this disgusting code:

(defmacro fn-traced
  [& definition]
  (let [conformed (s/conform ::ms/fn-args definition)
        name      (:name conformed)
        bs        (:bs conformed)
        arity-1?  (= (nth bs 0) :arity-1)
        args+body (nth bs 1)]
    (if arity-1?
      (if (= :body (nth (:body args+body) 0))
        (if name
          `(fn ~name ~(:args (:args args+body))
             ~@(map (fn [body] `(dbgn ~body)) (nth (:body args+body) 1)))
          `(fn ~(:args (:args args+body))
             ~@(map (fn [body] `(dbgn ~body)) (nth (:body args+body) 1))))
        ;; else :prepost+body
        (if name
          `(fn ~name ~(:args (:args args+body)) (:prepost (nth (:body ~args+body) 1))
             ~@(map (fn [body] `(dbgn ~body)) (:body (nth (:body args+body) 1))))
          `(fn ~(:args (:args args+body)) (:prepost (nth (:body ~args+body) 1))
             ~@(map (fn [body] `(dbgn ~body)) (:body (nth (:body args+body) 1)))))

        )
)

danielcompton13:03:26

was wondering if there's a pattern for doing this in a cleaner way

mpenet13:03:22

small critique, seems like the if name bits could be done inside the (fn ... part to avoid repeating the whole fn expression

mpenet13:03:32

both times

mpenet13:03:45

or I might be missing something

danielcompton13:03:01

but if there is no name then I'll end up with (fn nil [] ...)

mpenet13:03:48

oh right, I didn't pay attention

danielcompton13:03:12

Am I possibly approaching this whole thing in the wrong direction?

danielcompton13:03:23

Feels pretty dirty

danielcompton14:03:25

I'll be able to clean this up a lot, but here's how it turned out:

(defmacro fn-traced
  [& definition]
  (let [conformed (s/conform ::ms/fn-args definition)
        name      (:name conformed)
        bs        (:bs conformed)
        arity-1?  (= (nth bs 0) :arity-1)
        args+body (nth bs 1)]
    (if arity-1?
      (if (= :body (nth (:body args+body) 0))
        (if name
          `(fn ~name ~(:args (:args args+body))
             ~@(map (fn [body] `(dbgn ~body)) (nth (:body args+body) 1)))
          `(fn ~(:args (:args args+body))
             ~@(map (fn [body] `(dbgn ~body)) (nth (:body args+body) 1))))
        ;; else :prepost+body
        (if name
          `(fn ~name ~(:args (:args args+body)) ~(:prepost (nth (:body args+body) 1))
             ~@(map (fn [body] `(dbgn ~body)) (:body (nth (:body args+body) 1))))
          `(fn ~(:args (:args args+body)) ~(:prepost (nth (:body args+body) 1))
             ~@(map (fn [body] `(dbgn ~body)) (:body (nth (:body args+body) 1))))))
      ;; arity-n
      (let [bodies (:bodies args+body)]
        (if name
          `(fn ~name
             ~@(map (fn [a+b]
                      (println "A+B" (:body a+b))
                      (if (= :body (nth (:body a+b) 0))
                        `(~(:args (:args a+b))
                           ~@(map (fn [body] `(dbgn ~body)) (nth (:body a+b) 1)))
                        ;; prepost+body
                        `(~(:args (:args a+b))
                           ~(:prepost (nth (:body a+b) 1))
                           ~@(map (fn [body] `(dbgn ~body)) (:body (nth (:body a+b) 1))))
                        ))
                    bodies))
          `(fn ~@(map (fn [a+b]
                        (println "A+B" (:body a+b))
                        (if (= :body (nth (:body a+b) 0))
                          `(~(:args (:args a+b))
                             ~@(map (fn [body] `(dbgn ~body)) (nth (:body a+b) 1)))
                          ;; prepost+body
                          `(~(:args (:args a+b))
                             ~(:prepost (nth (:body a+b) 1))
                             ~@(map (fn [body] `(dbgn ~body)) (:body (nth (:body a+b) 1))))
                          ))
                      bodies)))))))

🎉 4
danielcompton14:03:04

@mpenet here's how to not repeat the name section twice:

`(fn ~@(when name [name])
           ~@(map (fn [a+b]
                    (if (= :body (nth (:body a+b) 0))
                      `(~(:args (:args a+b))
                         ~@(map (fn [body] `(dbgn ~body)) (nth (:body a+b) 1)))
                      ;; prepost+body
                      `(~(:args (:args a+b))
                         ~(:prepost (nth (:body a+b) 1))
                         ~@(map (fn [body] `(dbgn ~body)) (:body (nth (:body a+b) 1))))
                      ))
                  bodies))

danielcompton14:03:26

Wrap a single element in a collection and then unsplice it

Andreas Liljeqvist14:03:05

what would be the most idiomatic way of expressing xor for two keys in a map?

Alex Miller (Clojure team)14:03:16

it is possible to actually define xor and use it in :req, but I do not guarantee that that will work forever

Alex Miller (Clojure team)14:03:27

separately you can s/and around the map to enforce a predicate

danielcompton14:03:29

That's a bit better:

(defn fn-body [args+body]
  (if (= :body (nth (:body args+body) 0))
    `(~(or (:args (:args args+body)) [])
       ~@(map (fn [body] `(dbgn ~body)) (nth (:body args+body) 1)))
    ;; prepost+body
    `(~(or (:args (:args args+body)) [])
       ~(:prepost (nth (:body args+body) 1))
       ~@(map (fn [body] `(dbgn ~body)) (:body (nth (:body args+body) 1))))))

(defmacro fn-traced
  [& definition]
  (let [conformed (s/conform ::ms/fn-args definition)
        name      (:name conformed)
        bs        (:bs conformed)
        arity-1?  (= (nth bs 0) :arity-1)
        args+body (nth bs 1)]
    (if arity-1?
      `(fn ~@(when name [name])
         ~@(fn-body args+body))
      ;; arity-n
      (let [bodies (:bodies args+body)]
        `(fn ~@(when name [name])
           ~@(map fn-body bodies))))))

bbrinck19:03:47

Apologies if this doesn’t work in your case, but would this approach work? We’ve used it successfully to insert timers around function bodies http://blog.klipse.tech/clojure/2016/10/10/defn-args.html

bbrinck19:03:07

The trick is to modify the conformed value and then call unform to go back to valid clojure code

bbrinck19:03:04

Even if I deleted the docstrings, examples, and workaround for the spec bug, I think my code would longer than your solution, but here’s an example of the approach described in that link https://gist.github.com/bhb/128bf97619e83541a8adda7094bc370d

danielcompton20:03:55

Thanks, that’s a very clever way to do it

borkdude15:03:39

By mistake, but it surprised me:

(s/def :dropdowns/options (s/coll-of :dropdowns/options))
(s/valid? :dropdowns/options []) ;; true, ok, I can get that
(s/valid? :dropdowns/options [[]]) ;; true, uuh...

guy15:03:18

:thinking_face:

Alex Miller (Clojure team)15:03:22

coll-of doesn’t limit cardinality by default

borkdude15:03:38

yes, I get that

Alex Miller (Clojure team)15:03:49

Use :count, :min-count etc if needed

guy15:03:03

what is your spec for :dropdown/option ? suprised it allowed [[]]

borkdude15:03:17

no, you see, it’s recursive

guy15:03:24

oh right

guy15:03:46

Sorry missed that

borkdude16:03:18

I missed that too, but then I was surprised about this behavior, because spec will allow arbitrary nesting of empty colls this way

borkdude16:03:43

I don’t know if set theory is happy with this

borkdude16:03:47

the way to justify: an empty coll is a valid :foo, a collection of empy collections is a collection of valid :foo, hence an arbitrarily nested empty coll is a valid :foo

borkdude16:03:29

so actually this is a spec for arbitrarily nested empty colls

mpenet16:03:21

if you re thinking about monoid instances, it's perfectly valid imho

borkdude16:03:03

well if you think about types, a [a] isn’t the same as [[a]]

mpenet16:03:18

yeah but the mempty of [a] is []

mpenet16:03:49

but yes, no type annotation to validate that [] is indeed potentially a coll of foo

mpenet16:03:57

but other than that it's ok

borkdude16:03:48

it’s an unexpected way to describe all possible sets of empty sets

borkdude16:03:54

(s/valid? ::empty-nested [[] []]) is also true of course

mpenet16:03:57

spec cannot go that far. same problem with (s/def ::first-name string?) (s/def ::last-name string?)

mpenet16:03:34

using ::first-name instead of ::last-name in an fdef args signature will be silent, in haskell it would scream at you

mpenet16:03:50

same shape, not necessary same meaning

borkdude16:03:27

yes, I guess that makes sense. it’s enabled things too

mpenet16:03:54

no such thing as newtype or some kind of wrapper for validation

mpenet16:03:07

yes, it's all trade-offs I guess

borkdude16:03:32

How would you write this:

(s/def :dropdown/option (s/keys :req-un [:dropdown-option/id
                                         :dropdown-option/label]))
dropdown-option/id or dropdown/option-id . You can only namespace one level

guy16:03:45

>dropdown-option/id Says to me, thats the id of the dropdown option. >dropdown/option-id Says to me, thats the option-id of the dropdown

mpenet16:03:53

I use a little helper and create a new namespace for it

mpenet16:03:09

(rel-ns 'dropdown.option)

mpenet16:03:22

then ::dropdown.option/id

mpenet16:03:44

That's what I found to be the least horrible so far.

borkdude16:03:49

I could just use the dot anyway right? without creating a namespace?

mpenet16:03:19

you can use a keyword with a dot yes

borkdude16:03:25

ok, I’ll do that then

Roman Liutikov21:03:38

Is it possible that both s/conform and s/valid are failing, but s/explain returns Success!?

Roman Liutikov21:03:01

Not doing anything fancy. Clojure 1.9.0

Roman Liutikov21:03:25

ok, nevermind, forgot to reload ns

Alex Miller (Clojure team)23:03:15

any case of that is a bug