Fork me on GitHub
#clojure-spec
<
2017-03-24
>
manu10:03:10

I've just started learning clojure-specs. My question is: what is the right place to put them? in a different namespace or right next to the production code?

mpenet11:03:13

Personally I like to put them in a separate namespace, but there's no rule about it

Yehonathan Sharvit12:03:42

what are the pros/cons of having in same/different namespaces?

mpenet12:03:35

easier to exclude if it's in another namespace

mpenet12:03:01

other than that it's a matter of taste, diff languages do it both ways (ex haskell vs ocaml)

hospadar12:03:05

Q: is there any documentation of the Spec protocol? I ask because I'm building a couple implementations of it to wrap up some of my more complex predicates. Reason being I want to generate custom error messages that are more descriptive

hospadar12:03:48

I think I've mostly figured it out, but I'm mainly just guessing based on my reading of the implementations already in clojure.spec

hospadar12:03:25

specific problem: I'm validating a tree-structure that's defined in an EDN file. Not only do I want to validate that the nodes are all structurally OK, I need to validate certain properties of the graph as a whole like "are all the edges pointing at valid nodes" "is it acyclic?"

hospadar12:03:58

My first approach was to just write those validators completely outside of spec, but that makes them hard to compose (like if I pack this datastructure inside of another one)

hospadar12:03:16

My second thought was "just use predicates", but an error message like {:pred (not (contains-cycles? graph)) :obj <the whole graph>} is super unhelpful to a user trying to figure out which node is invalid

hospadar12:03:39

So instead I packed the predicates into a (reify Spec ...) and it's GREAT but it took me a while to figure out a) that there was even a protocol that I could implement b) how to implement it

mpenet12:03:23

I did something similar (spec that validates a dsl: parses, potentially generates error messages with line/num syntax helpers etc)

mpenet12:03:48

basically a reified Spec with a custom explain*

mpenet12:03:56

+ some caching not to parse again for explain

hospadar12:03:06

what I guess I'm really angling at is "If I wanted to contribute some helpful documentation of the spec protocol, would there be interest in receiving that, and how would I go about doing it?"

hospadar13:03:07

I have some other related minor thoughts like "maybe explain-1 shouldn't be private so that Spec implementors can use it without having to #'hack/it"

hospadar13:03:33

yeah @mpenet same thing for me too

hospadar13:03:56

I have a little custom lispey transform language I'm using to do some basic filtering that gets packed inside other specs

mpenet13:03:23

I am not sure what's the status of the Spec protocol, might be considered an (unreliable) implementation detail. Not sure but at the time @alexmiller advised me to do this so I hope not

hospadar13:03:20

yeah I suspected maybe that was the case

hospadar13:03:25

(or might be)

hospadar13:03:32

maybe a better approach would be extending clojure.spec/spec to take an (optional) custom explain function

hospadar13:03:40

in the same way it takes an optional generator

hospadar13:03:57

it would probably make my code look a little simpler šŸ˜›

mpenet13:03:30

it would be nice yes

Alex Miller (Clojure team)13:03:32

@hospadar the Spec protocol is subject to violent change without warning and weā€™re not interested in documenting it at the current time

Alex Miller (Clojure team)13:03:04

we may not even keep it

Alex Miller (Clojure team)13:03:10

I donā€™t think Rich is interested in having custom explain messages (but I could be wrong)

Alex Miller (Clojure team)13:03:16

I think Richā€™s conception is that in general, people should not be writing their own spec impls, they should be composing the provided specs

souenzzo13:03:47

(def my-keys [::a ::b ::c])
=> #'my-ns/my-keys
(s/def ::my-keys (s/keys :opt my-keys))
CompilerException java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol, compiling:(/tmp/form-init5422364345568681610.clj:1:18) 
Rly? I will need to do a macro over your macro? Why not allow data on spec declaration? šŸ˜¢

hospadar13:03:05

@souenzzo you can probably unescape (so long as the list is available at compile time as in your example

hospadar13:03:16

(s/def ::my-keys (s/keys :opt ~my-keys))

hospadar13:03:56

Use that trick in my project.clj to parameterize versions

dergutemoritz13:03:21

That only works if the macro explicitly allows it

hospadar13:03:21

@alexmiller I feel that, I'd really rather not be writing custom preds. But in some cases I have to and it's nice if I can pack them into my specs with more error detail than just "this predicate failed"

mpenet13:03:08

well you can (eval (s/def ::foo ~(...)), which is quite horrible

souenzzo13:03:24

(eval `(s/def ::my-keys (s/keys :opt ~my-keys)))
works, but (s/def ::my-keys (s/keys :opt ~my-keys)) no

mpenet13:03:10

hopefully in the future we can conform the spec form, modify it, then unform it

yonatanel13:03:31

@souenzzo What's your use case for defining keys separately? Are you reusing them?

mpenet13:03:03

wouldn't hold my breath tho

yonatanel13:03:18

@hospadar Would recursive spec solve your issue of informative tree validation explain message?

Alex Miller (Clojure team)13:03:40

CLJ-2112 is work that Rich asked me to do - thatā€™s something we will definitely have

hospadar14:03:22

@yonatanel sometimes yes, sometimes no (at least I'm not sure how I'd implement that right now?)

hospadar14:03:30

I'd love to be proven wrong though

hospadar14:03:04

that's a super-dumb simple example of the kind of problem I'm trying to validate

hospadar14:03:18

it's pretty easy for me to write a validator (in the form of a predicate) that just reduces over the edges to check their validity, and I can s/and that together with the structural spec at the top level

hospadar14:03:02

but I want to get a more helpful error message that says "this edge failed validation because this node isn't valid"

hospadar14:03:38

hence why I've started writing implementations of Spec that let me emit more specific error messages

yonatanel15:03:07

@hospadar I wonder if there should be a line between what you should validate with spec and what should go to another validator, other than runtime dependencies such as a database. In your case the rules might be a bit complex but you don't need anything but the data, so spec sounds good, but you can't compose yourself out of every case, so if spec already supports custom predicates and generators, why not explainers?

hospadar15:03:00

yeah that's my thought - if I was going out over the wire during validation ("only valid if exists in DB" situations) I'd probably not attempt to jam it in the spec

hospadar15:03:24

spec is very attractive to me for this kind of problem for me though since it makes it easy to build composable validators and still get helpful robust error reporting

hospadar15:03:09

I feel like spec should be able to validate (with useful errors) any data structure where the correctness of the data-structure doesn't rely on any external factors?

hospadar16:03:13

(i.e. the validation is very deterministic data->spec->answer is the same every time)

luxbock19:03:01

I kind of wish that a spec defined using s/and did not perform conforming while passing the value through its specs

luxbock19:03:21

I find myself defining named predicates that add additional constraints on what the value should be like using s/and, and it would be more natural for those predicates to receive the unconformed value

luxbock19:03:07

I know I could in theory use s/unform inside my predicates, but often times I already have an existing predicate that I use elsewhere as a part of the business logic of the program, so now I have to create a wrapper in order to use it with spec

luxbock19:03:30

user> (s/explain (s/and (s/cat :a int?) (fn [[a]] (> a 1))) [2])
UnsupportedOperationException nth not supported on this type: PersistentArrayMap  clojure.lang.RT.nthFrom (RT.java:962)

Alex Miller (Clojure team)19:03:56

@luxbock we have talked about providing both flowing and non-flowing versions of s/and

seancorfield19:03:06

@alexmiller and something for s/or that doesnā€™t produce pairs? (so we donā€™t need (s/conformer val) when we donā€™t care about the label)

Alex Miller (Clojure team)19:03:40

well s/nonconforming exists as a theoretical answer to that

Alex Miller (Clojure team)19:03:51

but Iā€™m not sure whether Rich considers it a good idea

Alex Miller (Clojure team)19:03:06

(s/conform (s/nonconforming (s/or :a int? :b string?)) 100)
=> 100

seancorfield20:03:33

Maybe Iā€™ll chat to him next week in Portland about it. Iā€™m loathe to rely on s/nonconforming since it may go away...

tjtolton20:03:42

does core.spec have any kind of spec that validates emails?

seancorfield20:03:51

@tjtolton Thatā€™s kind of a hard problem. You can use regexes, but it depends how ā€œcorrectā€ you want to be?

tjtolton20:03:18

right, haha, which is why I was vaguely hoping there would be some out of the box email validators and generators

tjtolton20:03:44

We can dev one ourselves, its no problem

seancorfield20:03:46

Iā€™ll share the regex we use:

(def email-regex
  "Sophisticated regex for validating an email address."
  (-> (str "(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|"
           "(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|"
           "(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))")
      re-pattern))

tjtolton20:03:19

yeah, then the trick is writing a generator for it

seancorfield20:03:22

Then

(defn valid-email?
  "Return true if the given string looks like an email address."
  [s]
  (boolean (re-matches email-regex s)))
and
(s/def ::email (s/with-gen (s/and (bounded-string 5 128)
                                  wstr/valid-email?)
                 (wgen/fn-string-from-regex wstr/email-regex)))
where fn-string-from-regex is a wrapper for test.chuckā€˜s string-from-regex generator.

seancorfield20:03:03

It generates some really wild email addresses šŸ™‚

tjtolton20:03:52

string from regex

tjtolton20:03:54

that's neat

seancorfield21:03:35

@tjtolton Depending on what youā€™re doing with generated emails, you might want a simpler regex for the generator portion ā€” hereā€™s just two examples from s/exercise: ā€ņ‚‹•ņ¤¹ņ±µŗš§¶Š@[858.0.376.98]" ā€\"ń³­Šš±¢†ņ˜Ŗ„󱬈ņ“–æņ¬œ†\"@5cl84.e.a2.QN-.hpr1s.H.Ikp"

seancorfield21:03:56

Hmm, the unicode didnā€™t survive the copyā€™nā€™paste...

seancorfield21:03:39

Anyway, you get the idea.

tjtolton21:03:59

Yeah, I do, thanks!

tjtolton21:03:09

Test.chuck is pretty neat

don.dwoske21:03:32

Newbie here. Iā€™m trying to write a multi-spec to handle a case like this:

{
            ::id "1"
            ::type "person"
            ::properties {
                ::firstName "Elon"
                ::lastName "Musk"
                ::email ""
              }
}

{
            ::id ā€œ2"
            ::type ā€œhouse"
            ::properties {
                ::color ā€œred"
                ::style ā€œVictorian"
              }
}
For a particular type value, I want the acceptable properties keys to be different, while every instance has an :id, :type and a few other fields. The properties are variable, but not the parent/core keys. In the examples Iā€™ve seen for multi-specs the type is always inside the variable collection itself.. e.g. Iā€™d need to move type into properties instead of leaving it at the top level.

Paco21:03:54

I think that you can perfectly use a multi-spec with the type where it is. Your entity specs will all have id, type and properties. No need to complicate things IMO

don.dwoske22:03:36

likely - but as I said - newbie - I donā€™t know how to do it. Iā€™m fooling around with using a nested s/spec in there to get the properties map returned for each type in the multi-spec.