Fork me on GitHub
#clojure-spec
<
2018-02-01
>
andy.fingerhut08:02:24

In Rich Hickey's Spec-ulation talk, he mentions the idea of wishing to make specs include some notion of side effects, for functions that have them: https://github.com/jafingerhut/jafingerhut.github.com/blob/master/transcripts/2016-dec-rich-hickey-spec-ulation.txt#L563-L569

andy.fingerhut08:02:49

"A function provides its return. If you gave me what I required, I will provide to you this result. And of course I would like to broaden this discussion to include services and procedures, and things like that. So if your thing is effectful, one of the things you provide is that effect. If you call this thing with these arguments, the thing will be in the database, or I will send an email for you, or some other thing."

andy.fingerhut08:02:13

Does anyone have any ideas on how such a thing might be described in a spec-like fashion?

mattford11:02:15

So I've been using https://github.com/ring-clojure/ring-spec to validate http responses.

mattford11:02:08

I'd like to over-ride/extend the body part of the above spec to use spec's I've written that model the JSON body I get.

mattford11:02:18

Is that a thing?

mattford11:02:02

How'd people go about that?

borkdude11:02:57

I’d write a different spec for the parsed body.

misha12:02:55

@bbrinck quick expound question: is there a function, accepting vanilla spec/explain-data value and returns pretty error message as string?

bbrinck15:02:18

@misha Yep, that should work. I’ll add that to my notes about what to add to public API for the expound beta

nwjsmith19:02:57

I'm having trouble getting a useful spec together for this map-entries function I've written:

(defn map-entries
  "Returns a map consisting of the result of applying f to the first entry of
  the map, followed by applying f to the second entry in the map, until the map
  is exhausted."
  [f m]
  (into {} (map f) m))

(s/fdef map-entries
  :args (s/cat :f (s/fspec :args (s/cat :entry (s/tuple any? any?)) :ret (s/tuple any? any?))
               :m (s/map-of any? any?))
  :ret (s/map-of any? any?)
  :fn #(<= (count (-> % :ret)) (count (-> % :args :m))))

nwjsmith19:02:23

If I turn instrumentation on, and evaluate say (map/map-entries (fn [[k v]] [(name k) (dec v)]) {:a 1 :b 2 :c 3}), I get an instrumentation exception because spec tries to run (fn [[k v]] [(name k) (dec v)]) with a generated (s/tuple any? any?).

nwjsmith19:02:15

If I loosen the spec on the :f arg to just fn?, then instrumentation is okay, but I'll lose a the "free" property tests. I guess I can just supply my own generator for check...

misha19:02:10

what would you gain, if you mock your f?

nwjsmith19:02:00

Well if I had the following spec:

(s/fdef map-entries
  :args (s/cat :f fn?
               :m (s/map-of any? any?))
  :ret (s/map-of any? any?)
  :fn #(<= (count (-> % :ret)) (count (-> % :args :m))))

nwjsmith19:02:39

Then any function could pass instrumentation (not awesome).

misha19:02:05

I think you can at least spec f's args being a tuple, and stop there

mattford19:02:13

I have this map

{"took" 1,
 "timed_out" false,
 "terminated_early" false,
 "_shards" {"total" 1, "successful" 1, "failed" 0},
 "hits" {"total" 0, "max_score" nil, "hits" []}}
can I have spec conform the strings to keywords and deal with the overloaded "hits" key somehow?

misha19:02:04

I think, strings to keywords conversion needs to happen separately. but later, you'll be able to use 2 :keys keyword specs with different namespaces

nwjsmith19:02:25

Maybe I can spec the args differently here. What I'd like is the spec to be is

(s/cat :f (s/fspec :args (s/cat :entry (s/tuple <T: any?> <V: any?>))
                   :ret (s/tuple any? any?))
       :m (s/map-of <T: any?> <V: any?>))

nwjsmith19:02:38

ugh, let me format that

misha19:02:17

@mattford

(def m {"took" 1,
        "timed_out" false,
        "terminated_early" false,
        "_shards" {"total" 1, "successful" 1, "failed" 0},
        "hits" {"total" 0, "max_score" nil, "hits" []}})
(s/def :foo/hits vector?)
(s/def :bar/hits (s/keys :req-un [:foo/hits]))
(s/def :bar/m (s/keys :req-un [:bar/hits]))

(->> m
  (clojure.walk/keywordize-keys)
  (s/explain :bar/m))
Success!
=> nil

misha19:02:36

@nwjsmith try to drop :ret (s/tuple any? any?) for :f

misha19:02:19

into's spec might catch not-tuples from f

misha19:02:53

it blows up opieop

nwjsmith19:02:50

(defn map-entries
  "Returns a map consisting of the result of applying f to the first entry of
  the map, followed by applying f to the second entry in the map, until the map
  is exhausted."
  [f m]
  (into {} (map f) m))

(s/fdef map-entries
  :args (s/cat :f (s/fspec :args (s/cat :entry (s/tuple any? any?)))
               :m (s/map-of any? any?))
  :ret (s/map-of any? any?)
  :fn #(<= (count (-> % :ret)) (count (-> % :args :m))))


(stest/instrument)

(map-entries (fn [[k v]] [(name k) (dec v)]) {:a 1 :b 2 :c 3})

ExceptionInfo Call to #'user/map-entries did not conform to spec:
In: [0] val: ([nil nil]) fails at: [:args :f] predicate: (apply fn)
  clojure.core/ex-info (core.clj:4617)

nwjsmith19:02:38

How does instrumentation of function arguments work? Does the function get evaluated?

Alex Miller (Clojure team)20:02:41

the args spec for the function argument is used to generate inputs. the function argument function is invoked with those examples and the ret spec is validated

Alex Miller (Clojure team)20:02:11

also, you should pretty much never use fn? (if you go that route) - ifn? is almost always what you want

Alex Miller (Clojure team)20:02:21

generic higher-order functions are inherently difficult to spec in useful ways. specs are great for saying concrete things about your data. The more generic your function, the harder it is to say something specific and meaningful.

ghadi21:02:10

not sure if this is a bug, but I could use a second set of eyes:

ghadi22:02:21

I'm asking spec to stub out the function TARGET while overriding the generator for ::bar, which is aliased to ::foo. The gen overriding doesn't work -- if I override ::foo (which ::bar aliases) then it works. Probably a non-minimal example if someone can help me reduce it