Fork me on GitHub
#clojure-spec
<
2018-02-21
>
richiardiandrea01:02:45

regarding multi-arity functions, can I spec a 4-arity fn this way?

(s/fdef select-events-sqlmap
  :args (s/cat :domain :event/domain
               :entity :event/entity
               :key (s/alt :arity-2 nil? :arity-3-4 :event/key)
               :version (s/alt :arity-2-3 nil? :arity-4 :event/version)))
It seems not to work when I pass three args

richiardiandrea01:02:23

if there is a more idiomatic way I am open to it, this seems quite difficult to read

alexmiller01:02:49

(s/fdef select-events-sqlmap
  :args (s/cat :domain :event/domain
               :entity :event/entity
               :arity3-4 (s/? (s/cat :key :event/key :version (s/? :event/version)))))

richiardiandrea01:02:33

Thanks a lot I will try this out!

richiardiandrea17:02:05

tried this, but it seems to fail when :version is nil, will play more with it

alexmiller17:02:18

you can wrap (s/nilable <spec>) around a spec to also allow for nil

richiardiandrea18:02:30

this is what works:

(s/fdef select-events-sqlmap
  :args (s/cat :domain :event/domain
               :entity :event/entity
               :arity3-4 (s/? (s/cat :key (s/nilable :event/key)
                                     :version (s/? (s/nilable :event/version))))))

alexmiller01:02:43

you can also s/alt at the top for the 2, 3, and 4 cases if you want a single map with the same keys conformed out

richiardiandrea17:02:24

s/alt only works with keywords so does that mean that I need to break the spec in three?

alexmiller17:02:53

s/alt works with any spec - keywords are used to tag the alternatives

hawari08:02:09

Can I make a spec definition that depends on another spec like:

(spec/def ::start-time
  (spec/and string? valid-datetime?))
(spec/def ::end-time
  (spec/and string? valid-datetime?)) ;; need to validate that it's greater than start-time

(spec/def an-entity
  (spec/keys :req-un [::start-time ::end-time]))

moxaj09:02:38

@hawari.rahman17 you should do that validation in the ::an-entity spec

hawari09:02:47

Can you suggest on how can that be achieved? My apologies, I've just recently started using spec, I don't fully grasp its concept yet. Do I use spec/and and pair the spec/keys with another predicate which validate that start-time is greater than end-time?

hawari09:02:48

If so, how can one destructure a map from within a predicate? Does it get treated as an argument to a spec? @moxaj

moxaj09:02:22

(spec/and (spec/keys ..) (fn [{:keys [start-time end-time]}] ...))

moxaj09:02:49

your predicate is just a regular function

hawari09:02:31

Ah great, I'll try it right away, thank you very much for your suggestion @moxaj!

yogidevbear13:02:50

Hi everyone. I'm a spec n00b so please have patience with my question here. Can/should I use spec for validating user input passed into my functions? For example, if I have a function that should accept positive integer values only and those value might need to be within a particular range, would spec work for this (and possibly what might the general structure of code look like)?

yogidevbear14:02:26

Thanks Talyor 👍 I'll check that out now

alexmiller14:02:20

also check out s/int-in for your range spec

Olical14:02:06

Considering it makes a very very good parser for all sorts of data structures, that'd probably be a fine use case. Since the result is in itself more data, you could add more constraints in the future then build your error messages from that output.

Olical14:02:28

Also, by user input do you mean an actual form on a page, or as an API?

yogidevbear14:02:38

At the moment, user input from a REPL.

yogidevbear14:02:00

via running lein repl and the calling the functions from the REPL

Olical14:02:26

Ah, so you're talking runtime, not dev time, which is a big difference. At dev time you can just define your function specs and the user can decide to enable instrumentation (in my opinion). At runtime you'll be able to run your inputs through conform and respond with an informative error if it doesn't fit.

yogidevbear14:02:11

Can you explain what you mean by >the user can decide to enable instrumentation

Olical14:02:29

Sure! So, if you define specs for all of your functions with s/fdef, nothing actually happens at runtime. It's just sort of there, like documentation.

Olical14:02:55

The user, or developer, can decide to instrument a namespace / all namespaces so that these fdef specs are checked.

Olical14:02:17

In normal spec, this only means the arguments being passed to functions are checked, but you can use something like Orchestra to check the return values too.

Olical14:02:14

You may find https://github.com/bhb/expound interesting, it makes spec errors human readable at a glance. And here's https://github.com/jeaye/orchestra

yogidevbear14:02:37

Cool, thanks Oliver 🙂

Olical14:02:47

No problem. I think you'll want to use https://clojuredocs.org/clojure.spec.alpha/conform on your data and a spec that checks your int is in range. For which you can use https://clojuredocs.org/clojure.spec.alpha/int-in-range_q

Olical14:02:38

user=> (s/conform #(s/int-in-range? 5 10 %) 2)
:clojure.spec.alpha/invalid
user=> (s/explain #(s/int-in-range? 5 10 %) 2)
val: 2 fails predicate: :clojure.spec.alpha/unknown

Olical14:02:39

You can use (s/def ::my-num #(s/int-in-range? 5 10 %)) then (s/explain ::my-num 2) gives you a more descriptive error too. Not sure how much spec you know though, so sorry if I'm explaining too much 😅

yogidevbear14:02:32

At present, I know about 2 hours worth 🙂

yogidevbear14:02:44

So all the explanation is very welcome

yogidevbear14:02:08

And if I'm using clojure 1.9.0, do I simply add this to my ns?

(ns my-ns.core
  (:require [clojure.spec.alpha :as s]))

Olical14:02:47

It should be present anyway, it's included as part of Clojure. No need to depend on it afaik

Olical14:02:52

But yes, that require line is fine.

Olical14:02:00

And you can use cljs.spec.alpha in cljs

taylor14:02:01

> Clojure 1.9 depends on this library and provides it to users of Clojure. Thus, the recommended way to use this library is to add a dependency on the latest version of Clojure 1.9, rather than including it directly

yogidevbear14:02:04

Yeah, it seemed to be throwing some error on my spec function without needing to depend so looks like 1.9.0 includes it by defaut

yogidevbear14:02:55

I think I've got a spec working without throwing an error when I run lein repl. Excuse the trivial function example:

(defn my-fn
  "An example function. Takes two arguments and makes a vector from them"
  [a b]
  (into [] (list a b)))
(s/fdef my-fn
        :args (s/cat :a #(s/int-in-range? 1 11 %)
                     :b #(< 10 %)))

yogidevbear14:02:08

Does that seem correct?

Olical14:02:20

Looks right to me, yep!

Olical14:02:32

You can put the fdef before the defn if you want to btw

Olical14:02:50

And to get clojure to check this you have to use spec tools instrument or something like orchestra

Olical14:02:21

So these are more of a developer tool thing, if you want to check things at runtime that you present to the user, you probably want s/conform and s/explain

yogidevbear14:02:51

So my next question then... if I run (my-fn 1 2) within the REPL, it works instead of throwing an error

yogidevbear14:02:09

Is this where the spec tooling you're referring to would come into play?

Olical14:02:16

Yep, this is where you have to instrument

Olical14:02:24

You don't want spec checking EVERY function call in prod

Olical14:02:26

So it's opt in

yogidevbear14:02:44

Ah, that makes a lot of sense 🙂

Olical14:02:24

Check out orchestra though, it has some really neat tooling https://github.com/jeaye/orchestra#defn-spec

yogidevbear15:02:51

@U38J3881W any ideas why I might be getting Could not locate orchestra/core__init.class or orchestra/core.clj on classpath when running lein repl? I have [orchestra "0.2.0"] in my dependencies and (:require [orchestra.core :refer [defn-spec]]) in my ns declaration

Olical15:02:02

Ooh, odd. Maybe it's a doc thing? I suppose it's not found in [orchestra.spec.test :as st]?

Olical15:02:51

Also, maybe the docs are wrong in another way, try [orchestra "2017.11.12-1"], that one seems to be for newer versions of Clojure.

yogidevbear15:02:55

Hmm, not sure. They don't seem to include orchestra.spec.test in their defn-spec example on that readme

yogidevbear15:02:13

Isn't that version the cljs version?

Olical15:02:24

0.2.0 says it's for "1.9.0 >= Clojure < 1.9.0-alpha16"

yogidevbear15:02:41

Let me try the other version and see

Olical15:02:00

Yeah, I think you want the other one, it's for CLJS and CLJ 🙂

Olical15:02:50

0.2.0 seems to be legacy for an older style of spec.

yogidevbear15:02:17

Thanks, that seems to have fixed it

Olical16:02:36

Nice! I hope it's all working well. Tried it with expound yet? Definitely makes the errors more palatable.

bfabry17:02:18

following on from hawari's example above, what's the best way to attach a generator to the compound spec that generates end-time by adding some random amount to start-time. assume the spec/keys call involves a lot of keys all the things that I'm coming up with have a lot of boilerplate, but I probably just don't understand test.check very well

gfredericks17:02:36

what do you have so far?

bfabry17:02:36

well I think I just ran into some unrelated cljs macro problems

bfabry17:02:40

but ignoring those

bfabry17:02:55

cljs.user=> (s/def ::call' (s/keys :req-un [::direction ::duration ::agent_id ::group_id ::id ::start_time ::end_time]))
cljs.user=> (def call-gen #(gen/let [e (s/gen ::call')] (assoc e :end_time (+ (:start_time e) (rand-int 3600)))))
(s/def ::call (s/and (s/with-gen ::call' call-gen) #(> (:end_time %) (:start_time %))))

johanatan19:02:41

hi, does spec have support for heterogeneous maps? in particular i want to validate a map like this: {:a-special-kwd [:some :elements] "string" ::some-structure "string2" ::some-structure ... }

alexmiller19:02:31

hybrid map example

johanatan19:02:21

i suppose i could do: #(s/and (valid-special? %1) (s/valid? (s/map-of string? ::some-structure) (dissoc %1 :a-special-kwd))) ?

johanatan19:02:37

is there a better way?

johanatan19:02:24

i suppose i was trying to avoid having lambdas which explicitly call valid? though (if there is a way)

aaron5119:02:27

Hello! How do we check return values using fdef and stest/instrument? I see you can add the :ret key, but it doesn't seem to fail when it should. Also can stest/instrument check all fdef'd functions (instead of having to list them all)?

taylor19:02:04

stest/instrument doesn’t check :ret specs, only :args AFAIK

johanatan19:02:29

stest/check does however

taylor19:02:35

and calling (stest/instrument) with no args should instrument everything that’s been loaded

aaron5119:02:29

Any way to check ret without auto generating tests with check? We're unit testing and trying to use specs on the return value

taylor19:02:04

I think someone has written some utility code for doing that, can’t remember which lib it’s in

taylor19:02:40

pretty sure that’s it ☝️

aaron5119:02:34

We were looking for a way to continue using defn, instead of defn-spec, because defn-spec confuses Cursive

taylor19:02:17

I don’t think you’re required to use defn-spec with orchestra

taylor19:02:01

i.e. just using orchestra’s instrument seems like it will do :ret checking on plain’ ol defns and s/fdefs (hopefully?)

aaron5119:02:10

oh wow, that's exactly the answer I needed. We were fighting with Orchestra and Cursive for a while

taylor19:02:22

FWIW (and it probably wouldn’t help you here) but Cursive has a feature where you can tell it to treat custom macros like the core/known macros, assuming the custom macro follows one of those known patterns

aaron5123:02:15

oh, we tried that. It partially worked, but then jumping between tests and implementation broke. Seems like the feature we want but it isn't quite fully baked yet

aaron5123:02:21

thanks again

gfredericks19:02:35

@bfabry ignoring the unrelated issues with using rand-int, I think the boilerplate you have is necessary given the current API maybe a helpful thing would be a variant of s/and that lets you modify the default generator of the first argument

bfabry19:02:22

@johanatan I think you're looking for s/keys

johanatan19:02:46

@bfabry no, that won't work. the "strings" (string1, string2) are dynamic/unknown

bfabry19:02:52

@gfredericks what are the issues with using rant-int and what should I be doing instead?

bfabry19:02:18

oh sorry I didn't realise you meant heterogeneous keys as well

johanatan19:02:26

yes, heterogeneous

gfredericks19:02:30

using your own randomness instead of a generator means you lose growth, reproducibility and shrinking; one way to address that is to add a temporary key to ::call' that maps to something that generates the sort of nonnegative integer you want, and then remove that key in ::call

bfabry19:02:59

can I just use duration (s/gen (s/int-in 1 3600)) in the gen/let and then use that?

gfredericks19:02:03

oh yeah that's a good idea

bfabry19:02:24

ok cool, slowly understanding