Fork me on GitHub
#clojure-spec
<
2016-11-01
>
settinghead03:11:02

I'd like to conform this vector [["John" 20] ["Jane" 30] ["Bob" 40]] into [{:name "John" :age 20} {:name "Jane" :age 30} {: name "Bob" :age 40}]. how should I write my spec?

curlyfry06:11:21

@settinghead

(def john-jane-bob [["John" 20] ["Jane" 30] ["Bob" 40]])
(s/def ::person (s/cat :name string? :age integer?))
(s/def ::persons (s/coll-of ::person :kind vector?))
(s/conform ::persons john-jane-bob) =>  [{:name "John", :age 20} {:name "Jane", :age 30} {:name "Bob", :age 40}]

dominicm15:11:02

How does clojure-spec work with the reloaded workflow / tools.namespace? I notice both that: 1. https://clojurians-log.clojureverse.org/clojure-spec/2016-09-09.html#inst-2016-09-09T18:34:11.000362Z 2. specs aren't/can't be, deleted or unloaded in any way. Any success with getting it to work in some way?

zane15:11:27

My experience has been that needing to unload a spec is a rare occurrence.

hiredman16:11:00

is there something like s/keys, but instead of looking up specs via global names, I can pass it a spec for the value of each key?

hiredman16:11:59

I have these tests that result in a data structure that is sort of a log of activity, and the result of the test is determined by checking that log

hiredman16:11:31

the log is a sequence of maps, and it has a :messages key, and at each entry in the log, I know that key should have a specific value

hiredman16:11:08

if I have to define the value globally (use s/def and s/keys) then all I can say is that it could be any of a number of different values

hiredman16:11:09

ah, maybe s/map-of and s/merge

bfabry17:11:05

@hiredman what do you mean "at each entry in the log", like you dynamically know what it should be or you have a list of things it should be in certain circumstances? because if the latter you could always use :req-un

hiredman17:11:03

:req-un still looks up specs via a global name in the spec registry

hiredman17:11:45

I have more refined information locally than globally

hiredman17:11:24

globally all I can say is the value for the key should be either x y or z, but locally I know exactly what the value for the key should be

bfabry17:11:29

right, but I meant you can define multiple versions of :messages using :req-un

bfabry17:11:17

(s/def ::messages-1 (s/keys :req-un [:messages-1/message])) and then switch in the spec you need later on

hiredman17:11:30

I was hoping to use spec as a sort of a data regex to validate these test results, but having to globally define map validators is a pain

hiredman17:11:07

like if you had to globally define your capture groups before you could use them in a regex engine

bfabry17:11:40

I see your point

hiredman17:11:35

and if I had followed "best practices" and namespaced the :messages key, that work around wouldn't work at all

hiredman17:11:03

also clojure.spec has a declare of map-spec, which is never defined https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj#L348 maybe a left over from a previous version

hiredman17:11:54

I guess I can build something using s/alt

madstap18:11:24

(s/fdef update*
    :args (s/cat :m map?
                 :k keyword?

                 ;; I want to say:
                 ;; The function needs to take at least one arg,
                 ;; the current value in the map,
                 ;; and can take zero or more extra arguments.
                 :f (s/fspec :args (s/cat :value-in-map any?
                                          :args (s/* any?)))

                 :args (s/* any?)))

  (defn update* [m k f & args]
    (apply update m k f args))

  (test/instrument)

  (update* {:a 10} :a inc)

madstap18:11:59

How do I do this in spec?

madstap18:11:35

;;=> Call to #'foo.core/update* did not conform to spec: In: [2]
  val: (nil) fails at: [:args :f] predicate: (apply fn)
  :clojure.spec/args ({:a 10} :a #function[clojure.core/inc])
  :clojure.spec/failure :instrument :clojure.spec.test/caller
  {:file "form-init8332152340913865653.clj", :line 342, :var-scope
   foo.core/eval12509}

zane18:11:26

@madstap You're probably better off just using a function.

madstap18:11:49

Like just :f ifn??

madstap18:11:10

From that example I'm having truble seeing how I could apply multi-spec to my problem

madstap18:11:12

@hiredman could you explain?

hiredman18:11:14

maybe not, multi-spec would only be useful if you knew the set of keys you would be pass to update*

hiredman18:11:03

I guess that is not entirely true, if you knew the set of keys entirely before hand, you could use an s/alt or s/or, the advantage using multi-spec would be that choice would be open ended

hiredman18:11:15

so easier to extend

hiredman18:11:40

(defmulti update-multi-spec (fn [m k fun & args] k))

(s/fdef update*
        :args (s/multi-spec update-multi-spec)
        :ret any?)

(defmethod update-multi-spec ::a [m k fun & args]
  (s/cat
   :map (s/keys :req [::a])
   :key #{k}
   :fun (s/fspec :args ...)
   :args ...
   ))

madstap19:11:39

I think I explained myself badly, what I want to do is say that the function passed to update needs to take at least one argument, and may take more.

madstap19:11:50

Your multi-spec example helped me grok why multi-spec is useful, though, thanks

madstap19:11:30

@hiredman Doesn't work..

(s/fdef update*
    :args (s/cat :m map?
                 :k keyword?
                 :f (s/fspec :args (s/+ any?))
                 :args (s/* any?)))

  (defn update* [m k f & args]
    (apply update m k f args))

  (test/instrument)

  (update* {:a 10} :a inc)

madstap19:11:26

Call to #'foo.core/update* did not conform to spec: In: [2]
  val: (nil) fails at: [:args :f] predicate: (apply fn)
  :clojure.spec/args ({:a 10} :a #function[clojure.core/inc])
  :clojure.spec/failure :instrument
  :clojure.spec.test/caller {:file
                             "form-init8954135211496312533.clj", :line 478, :var-scope
                             foo.core/eval13250}

hiredman19:11:01

yeah, because any? is generating bad inputs for the function

hiredman19:11:36

the way test check validates the function is it generates inputs for it

hiredman19:11:20

your function is inc, in the spec you say it can take an any?, so spec is checking that by feeding it all kinds of values

madstap19:11:13

Right, what I wan't to say that this function needs to take at least one arg, but doesn't necessarily take any more args.

madstap19:11:22

In this case that passing (fn [] (rand-int 10)) to update will fail, but inc will work, as will (constantly true).

hiredman19:11:17

the problem is when you turn instrumentation on, spec is validating inc against your fspec

hiredman20:11:17

a spec that is general enough isn't going to be valid against a specific function, you will generate very general inputs that the specific function won't handle

hiredman20:11:13

I think the best you could do would be a multi-spec, with a defmethod for every function/key combination you pass to update*

madstap20:11:16

Got it, thanks