Fork me on GitHub
#clojure-spec
<
2016-10-06
>
Oliver George00:10:24

@bfabry no promises that it's the most elegant approach. I'm looking forward to seeing how others work with spec forms.

mattly00:10:29

heh, I'm actually working on something to define graphql forms from specs definitions

jrheard00:10:16

unrelated: you’re using graphql? d’you like it?

jrheard00:10:33

i haven’t used it myself but the talks i’ve seen on it are pretty compelling

Oliver George00:10:51

@mattly that's on my radar too. We are replacing our simple "datomic style" pull api with graphql soon. Here's what I came up with generating recursive pull specs from clojure.spec forms: https://gist.github.com/olivergeorge/9d822447e4838092d07138ea51af5782

Oliver George00:10:27

(again, no promises that it's a good approach... really want to see other peoples code and learn some effective techniques)

mattly00:10:05

I'm going to keep my pull-style api, but aim to hookup a graphql parser to it

Oliver George00:10:45

That makes sense.

mattly00:10:27

mostly because some of my query endpoints, I need expressive arguments

Oliver George00:10:32

We bolted a "pull" arg to our REST endpoints which was a practical step forward at the time. Same old REST params etc but more flexibility around what data we return. Now that GraphQL is looking fairly stable we're intrested in standardising on it's interface. That decouples our frontend from the backend which should allow us some flexibility.

mattly00:10:22

I'm fairly lucky in that mine is a greenfield project

Oliver George00:10:05

Nice. We're in the enterprise space so typically the backend tech stack is dictated to us.

mattly00:10:52

I may try to abstract out my resolver into a library

mattly00:10:23

it has a separate planning stage to help with the n+1 problem

Oliver George00:10:40

@mattly I'd love to see what you come up with.

Oliver George00:10:54

I'm not sure it's related but I did some thinking around translating graph queries into flat sql queries.

mattly00:10:18

eh, removed the link, there's a bug in that

mattly00:10:39

I need to properly extract it

mikeb07:10:45

@olivergeorge any thoughts about reversing direction and generating spec definitions from a sql schema?

Oliver George09:10:15

@mikeb I've had some fun with that actually. It's easy to produce something for each column in a database but the relations are trickier.

Oliver George09:10:37

The slightly fiddly bit is that you can't really do it dynamically. At least I can't work out how to have my script generate and run macros. In the end I generated some forms and committed them as code.

Yehonathan Sharvit15:10:55

I was looking into clojurescript code and I saw that clojure/core/specs.clj from clojure is not there. Why clojure/core/specs.clj hasn’t been ported into clojurescript?

dnolen15:10:42

Just haven't gotten to it yet

Yehonathan Sharvit15:10:51

would you be interested in a patch?

mlimotte15:10:48

I see this in the spec guide: "check also takes a number of options that can be passed to test.check to influence the test run, as well as the option to override generators for parts of the spec, by either name or path." Any examples of the "path" option for generator overrides?

jrheard15:10:44

i’d love to see an example of generator override usage, path or no

mlimotte15:10:59

@jrheard here's a basic example with override by name:

(defn foo [x] (println "FOO" x) (inc x))
(s/fdef foo :args (s/cat :x ::x))
(s/def ::x clojure.future/int?)
(stest/check `foo
             {:gen          {::x #(gen/return 10)}
              ::stestc/opts {:num-tests 1}})

jrheard15:10:21

gen comes from clojure.test.check, not from spec, right?

mlimotte15:10:00

But not sure how to do it when the spec is defined like this:

(s/fdef foo :args (s/cat :x clojure.future/int?))

mlimotte15:10:18

[clojure.spec.gen :as gen]

dpiatek15:10:50

@mlimotte I think you need to do (s/fdef foo :args (s/cat :x (s/spec clojure.future/int?)))

mlimotte15:10:32

The foo spec above seems to work even w/out (s/spec ...). I'm still not sure how to do a generator override for this case where the spec is not named, though.

dpiatek15:10:53

Oh, I see - sorry, misread the conversation!

naomarik18:10:18

i'm sorry about this naive question: if i were to spec a map that has strings as keys or even another type, i would have to convert them to keywords first before passing them into spec?

mlimotte18:10:05

i don't think so, do you have an example of what you're trying to do?

bfabry18:10:17

@naomarik no. you need keywords for keys to use s/keys but there's still lots of other predicates that you can use to validate maps, including writing your own

naomarik18:10:44

i've been struggling to find an example for this, let's say just validating something simple like {"type" "tweet" "user" {"type" "registered"}}. I would know how to go about doing this from reading the official guide if these were keywords, but not like this

mlimotte18:10:46

i think you're saying that those are required keys. So, like bfabry said, you can't use s/keys. you have to write your own predicates to do that.

bfabry18:10:57

@naomarik it depends on what you want to validate. if you know what keys you're going to get in advance and what their semantics are and which ones are required then I'd recommend converting them to keywords. it just makes sense. if the keys are dynamic then I would leave them as strings and validating other properties about the map

mlimotte18:10:16

@naomarik if it's just required keys, a simple predicate isn't too bad:

(s/def ::my-map (s/and map? #(every? (partial contains? %)  #{"type" "user"})))
(s/valid? ::my-map {"type" "tweet" "user" {"type" "registered"}})
=> true
(s/valid? ::my-map {"type" "tweet" "NotUser" {"type" "registered"}})
=> false
If it's more complicated and you need all the features of s/keys, than writing your could be a hassle.

naomarik18:10:58

s/keys is a lot more convenient at that point

bfabry18:10:54

yeah like I said, if you know what keys your map contains I would convert them to keywords. for lots and lots of good reasons including that you get nice spec tools for working with them

naomarik19:10:10

i always write all my maps as keywords, but is there a discussion online why namespaced keywords are enforced so i can be enlightened?

bfabry19:10:10

there's been a few, I think maybe the cognicast podcast with rich hickey talking about spec might be the best bet. they're not enforced though

bfabry19:10:01

s/keys has corresponding :req-un and :opt-un options that you can use for un-namespaced keywords

mpenet19:10:38

Yeah but some stuff is brittle with un- keys ex broken conforming with s/merge

mpenet19:10:48

See recent discussion about it in history

bfabry19:10:35

alex's comments seem to indicate that's confusion over how conforming works with merge, and isn't related to namespaced keys

mpenet19:10:34

Wrong ticket, my google fu failed me

bfabry19:10:06

he's saying only the last spec in the merge conforms

bfabry19:10:22

ohh wait no I understand

bfabry19:10:53

ok, but there's no way to fix that

mpenet19:10:07

Never saw the point in ns keys personally.

bfabry19:10:21

you get behaviour for free when using namespaced keys. but it's just not possible if they're not namespaced

bfabry19:10:53

well. this would be one of the points. you get conforming for free

bfabry19:10:53

I think perhaps alex's example hasn't explained what's happening properly. this might help

boot.user=> (require '[clojure.spec :as s])
nil
boot.user=> (defn convert [n] (if (double? n) n (double n)))
#'boot.user/convert
boot.user=> (s/def ::value (s/conformer convert))
:boot.user/value
boot.user=> (s/conform (s/keys) {::value 5})
#:boot.user{:value 5.0}
boot.user=>

bfabry19:10:36

so even though s/keys doesn't specify ::value as req or opt, because ::value is namespaced and has a spec associated it automatically gets conformed and validated

bfabry19:10:09

so it's not that merge behaves differently with namespaced keys, it's that s/keys behaves differently with namespaced keys (it gives you free stuff if they're namespaced)

Yehonathan Sharvit19:10:36

Something weird about s/and:

user=> (s/def ::ff (s/cat :start integer? :end integer?))
:user/ff
user=> (s/def ::gg (s/* ::ff))
:user/gg
user=> (s/explain-str ::gg [4 7 2 1])
"Success!\n"
user=> (s/def ::ff (s/and (s/cat :start integer? :end integer?)))
:user/ff
user=> (s/explain-str ::gg [4 7 2 1])
"In: [0] val: 4 fails spec: :user/ff predicate: (cat :start integer? :end integer?)\n"

Yehonathan Sharvit19:10:06

Why in the second case - after adding s/and the vector doesn’t conform?

seancorfield19:10:34

Your ::gg spec is zero or more integer-followed-by-integer patterns.

seancorfield19:10:53

Because s/* and s/cat combine as a regex sequence.

seancorfield19:10:28

When you insert s/and you isolate the the pair spec so ::gg becomes zero or more pairs

seancorfield19:10:45

i.e., [[4 7] [2 1]]

Yehonathan Sharvit19:10:05

What is the way not to isolate the pair?

Yehonathan Sharvit19:10:36

My use case is that I need extra-validation

Yehonathan Sharvit19:10:40

(s/def ::ff (s/and (s/cat :start integer? :end integer?)
                   #(< (:start %) (:end %))))

(s/def ::gg (s/* ::ff))

seancorfield19:10:13

So you want a sequence of integers that is even in length and when you partition that sequence into pairs, each pair is ordered…?

seancorfield19:10:11

So (s/and seq-of-ints length-is-even every-partitioned-pair-is-ordered)

mpenet19:10:11

@bfabry Yep, thats what I meant. The fact that ns keys are so deeply rooted in spec makes this stuff (merge for instance) counter intuitive.

Yehonathan Sharvit19:10:01

@seancorfield But I want to do it in an idiomatic clojure.spec way

bfabry19:10:20

again, I'd say it's (s/keys) that's counter-intuitive, not s/merge, but yeah possibly the automatic conform/validate nature of namespaced keywords could be called out more

Yehonathan Sharvit19:10:26

Actually, my real need is to build a spec for sequence of integers that are palindromes...

Yehonathan Sharvit19:10:29

I tried:

(s/def ::short-palindrome 
  (s/and (s/cat :a integer?
                :rest ::palindrome
                :a integer?)
         #(= (:a1 %) (:a2 %))))

(s/def ::palindrome (s/* ::short-palindrome))

mpenet19:10:58

I know, I never said merged was bugged, but that s one of the places where this behavior shows through in a bad way

Yehonathan Sharvit19:10:32

But it only accepts nested sequences like [[1 [ 2 2] 1]] but not flat sequences like that: [1 2 2 1]

Yehonathan Sharvit19:10:50

how would you write the spec for palindrome @seancorfield ?

seancorfield19:10:11

For a sequence that is a palindrome? (s/and (s/coll-of integer?) #(= % (reverse %)))

Yehonathan Sharvit19:10:14

It works (except for empty sequences)

Yehonathan Sharvit19:10:38

I would like to write it like a context-free grammar

Yehonathan Sharvit19:10:22

Maybe that’s not the idea with clojure.spec @seancorfield ?

seancorfield19:10:50

You probably want to ask @alexmiller ...

Yehonathan Sharvit20:10:10

Now, I’m trying to implement the grammar for algebraic expressions - as described here https://en.wikipedia.org/wiki/Context-free_grammar#Algebraic_expressions:

Here is a context-free grammar for syntactically correct infix algebraic expressions in the variables x, y and z:

S → x
S → y
S → z
S → S + S
S → S - S
S → S * S
S → S / S
S → ( S )

Yehonathan Sharvit20:10:30

Do I have a chance to succeed @seancorfield and @alexmiller ?

Alex Miller (Clojure team)20:10:39

I would have written something like this for palindromes (& is much better than and here as it stays in regex)

Alex Miller (Clojure team)20:10:46

(s/def ::pal 
  (s/alt :0 (s/cat)
         :1 int?
         :n (s/& (s/cat :a int? :b ::pal :c int?) (fn [{:keys [a c]}] (= a c)))))

Alex Miller (Clojure team)20:10:42

you can use the same approach for your algebraic expressions

Yehonathan Sharvit20:10:56

I’m trying it now

Yehonathan Sharvit20:10:23

working on ::algebraic-expression...

Yehonathan Sharvit20:10:40

@alexmiller what’s the exact meaning of (cat) with no arguments?

Yehonathan Sharvit20:10:55

Is it like empty??

Alex Miller (Clojure team)20:10:13

it’s a sequential collection with no contents

Alex Miller (Clojure team)20:10:24

but it’s a regex so will compose better with other regex ops

Alex Miller (Clojure team)20:10:06

in particular, it will be treated in the same sequential context here, not be a separate (nested) collection

seancorfield20:10:16

Ah, yes, I still forget about s/&...

Yehonathan Sharvit20:10:51

@alexmiller Now, I’m having stackoverflow issues

Yehonathan Sharvit20:10:00

(s/def ::arithmetic
  (s/alt
    :var int?
    :plus (s/cat :a ::arithmetic :op #{"+"} :b ::arithmetic)))

(s/explain-str ::arithmetic [1 "+" 1])

Alex Miller (Clojure team)20:10:25

yeah, that's not going to work

Alex Miller (Clojure team)20:10:39

there are other typical approaches to writing left-recursive grammars like this

Yehonathan Sharvit20:10:53

left-recursion is not (yet) supported in clojure.spec?

Alex Miller (Clojure team)20:10:23

this is just a consequence of how the regex logic works

Yehonathan Sharvit20:10:12

you mean the deriv and accept-nil? functions

Yehonathan Sharvit20:10:13

- that implement the ideas of the “Parsing with derivatives” paper ?

Alex Miller (Clojure team)20:10:12

spec regex walks all viable alternatives in parallel - in this case, that becomes an ever-growing set

Alex Miller (Clojure team)20:10:24

but you can rewrite those kinds of grammars like

Alex Miller (Clojure team)20:10:28

(s/def ::ar (s/cat :a int? :r (s/? (s/cat :op #{"+”} :b (s/alt :i int? :e ::ar)))))

Alex Miller (Clojure team)20:10:17

in reality, a) these problems don’t come up that often and b) prefix-factoring is an option

Yehonathan Sharvit20:10:20

By “prefix-factoring” you mean that the recursion appears last or that it doesn’t appear first?

seancorfield21:10:44

@viebel you can see why I deferred to Alex on the "context-free grammar" aspect of clojure.spec, eh? 🙂

Yehonathan Sharvit21:10:11

Yeah! It’s hard stuff

Yehonathan Sharvit21:10:22

Now, I’m struggling with the parentheses

Yehonathan Sharvit21:10:43

(s/def ::my-int (s/* #{\0 \1 \2 \3 \4 \5 \6 \7 \8 \9 \x \y \z}))
(s/def ::ar (s/alt :operation (s/cat :a ::my-int      
                                     :r (s/? (s/cat :op #{\+ \*  \- \/}
                                                    :b (s/alt :i ::my-int :e ::ar))))
                   :parentheses (s/cat :o #{"("}
                                       :b (s/alt :i ::my-int :e ::ar)
                                       :c #{")"})))

Yehonathan Sharvit21:10:08

It works for (s/explain-str ::ar (seq "x+(y*x)”))

Yehonathan Sharvit21:10:35

But with (s/explain-str ::ar (seq "(y*x)+x”)) I get:

In: [5] val: ("+" "x") fails spec: :my.spec/ar predicate: (alt :operation (cat :a :my.spec/my-int :r (? (cat :op #{"+" "*" "-" "/"} :b (alt :i :my.spec/my-int :e :my.spec/ar)))) :parentheses (cat :o #{"("} :b (alt :i :my.spec/my-int :e :my.spec/ar) :c #{")"})),  Extra input

Yehonathan Sharvit21:10:28

@alexmiller please help (again) 😰

Alex Miller (Clojure team)21:10:03

Sorry I'm off duty for the night :)

Yehonathan Sharvit21:10:34

No problem: Actually I’m in Israel and it’s 12:30 AM - I’m too tired to continue with this tough topic. We’ll catch up later. If you find something please mention me on slack.

joshg21:10:13

Is there an idiomatic way to define a spec for a constant value? Something like (s/def ::some-constant #(identical? % "foo"))?

Yehonathan Sharvit21:10:55

You can use set like this: #{“foo”}

Yehonathan Sharvit21:10:12

because sets behave like functions

joshg21:10:12

That works, thanks

bfabry21:10:47

yes, sets are the idiomatic way to do that, sets also work as generators (while #(identical? % "foo") does not)