Fork me on GitHub
#clojure-spec
<
2017-03-28
>
danboykis00:03:29

Can anyone explain this? This works:

((fn []
   (if-let [k nil]
     k
     :other-keyword)))
=> :other-keyword
But this doesn't:
((fn []
  (if-let [k nil]
    k
    :clojure.spec/invalid)))
CompilerException clojure.lang.ExceptionInfo: Call to clojure.core/if-let did not conform to spec:
In: [2] val: :clojure.spec/invalid fails at: [:args :else] predicate: any?
...
The only difference being a keyword. I ran into this writing a conformer.

seancorfield00:03:27

:clojure.spec/invalid is a special value — you have to be really careful about code that produces it

seancorfield00:03:33

I think you can do (keyword “clojure.spec” “invalid”) as one way to get around that (i.e., generate it at runtime).

seancorfield00:03:47

(I suspect there’s a cleaner way)

danboykis00:03:22

seancorfield, I agree that it's special, I am using it in a conformer

danboykis00:03:10

this is a stripped down example

seancorfield00:03:10

It’s special to spec itself. And in 1.9, spec is used to syntax-check various macros.

danboykis00:03:11

in your opinion, is this expected behavior?

seancorfield00:03:41

Another option is

(def csi :clojure.spec/invalid)
…
((fn [] (if-let [k nil] k csi)))
And, yes, this is absolutely expected behavior.

seancorfield00:03:27

It is certainly a known issue but I don’t know whether there are plans to change the behavior.

danboykis00:03:01

it seems weird that there are special keywords i can't return from an if-let

seancorfield00:03:02

(I was actually a bit surprised that I could just def a global alias for it like that)

danboykis00:03:34

why not? it's just a keyword

danboykis00:03:44

oh ok great, someone filed it

seancorfield00:03:03

Known since last June and no indication that it really is a problem that needs to be fixed (and it has an easy workaround — that I’d simply forgotten!)

seancorfield00:03:54

(ironic that I was the one that provided the easy workaround in that ticket but I’d forgotten)

seancorfield00:03:13

so (if-let [k nil] k ':clojure.spec/invalid)

danboykis00:03:48

that's a cool workaround 🙂

seancorfield00:03:25

Since macros are just functions on code-as-data, spec can’t tell the difference between a conforming spec that produces :clojure.spec/invalid — the special value — and a literal :clojure.spec/invalid in the actual code.

danboykis00:03:25

i hope it get's fixed

danboykis00:03:08

the docs should probably change then

seancorfield00:03:10

I suspect the answer from the Clojure/core folks will be: if you’re writing a spec or a conformer that needs to produce the special invalid value, you must quote it.

danboykis00:03:38

it's not at all obvious that you're supposed to quote it

seancorfield00:03:49

You can return :clojure.spec/invalid in all sorts of constructs just fine. You only need to quote it in some situations.

seancorfield00:03:21

We have all sorts of specs and conformers that produce :clojure.spec/invalid as a literal value — we just don’t have code around it that is a macro that is checked by spec in the compiler.

seancorfield00:03:13

(if (some-check v) v :clojure.spec/invalid) works fine, as does (try (some-coercion v) (catch Exception _ :clojure.spec/invalid))

danboykis00:03:28

i wonder what will happen once cond is spec'ed too

danboykis00:03:50

you can return it now

danboykis00:03:52

but not later?

danboykis01:03:49

coming back to our discussion the crux of the issue is the following

danboykis01:03:53

(s/valid? (s/* any?) [::s/invalid])
=> false
(s/explain (s/* any?) [::s/invalid])
In: [0] val: :clojure.spec/invalid fails predicate: any?
=> nil
(any? ::s/invalid)
=> true

danboykis01:03:48

that's a contradiction

athos07:03:32

Sorry for breaking into the discussion. Does anyone know it's a known limitation or an unintended behavior that multi-spec in general cannot be unformed?

tjtolton14:03:08

Is there some kind of core.spec function for "generate a view of this data structure that conforms to this spec"? for instance

(s/def ::just-one-key (s/keys :req-un [::apples]))
(*mystery-function* ::just-one-key {:apples "apples" :bananas "bananas"})
;-> {:apples "apples"}

tjtolton14:03:35

conform is supposedly for destructuring, but perhaps my understanding of destructuring is wrong, because intuitively I feel like "select this known data pattern out of this unknown data" is what destructuring is for

tjtolton14:03:00

Basically, the use case here is "web request should conform to this data, but if you pass in other data, that's perfectly fine. But when we store the data off in the database, we should only select the part that we understand and care about."

tjtolton14:03:01

so, the spec's don't disallow keys, but we select the parts we're interested in for passing along

borkdude14:03:04

Recently I sometimes see error messages like these: adzerk.boot_cljs.util.proxy$clojure.lang.ExceptionInfo$ff19274a: ERROR: Attempting to call unbound fn: #'clojure.spec/macroexpand-check at file /Users/Borkdude/.boot/cache/tmp/…/app/1x42/fg7eat/public/js/app.out/ajax/core.cljc, line 591, column 1 I think it was after upgrading clojure.future.spec to alpha15

borkdude14:03:45

Not sure where it comes from

borkdude15:03:31

Now I get: Attempting to call unbound fn: #'clojure.spec/macroexpand-check at line 2164, column 1 in file /Users/Borkdude/.boot/cache/tmp/.../app/1xx5/-x902hh/public/js/app.out/cljs/pprint.cljs

borkdude15:03:37

Hmm, sorry, this was with alpha-14. Now I upgraded to clojure.future.spec alpha 15.

tjtolton15:03:39

this is crazy, it doesn't even make any sense to me. the guide here https://clojure.org/guides/spec seems to imply that conform is used for destructuring. Here's the example it gives:

(defn configure [input]
  (let [parsed (s/conform ::config input)]
    (if (= parsed ::s/invalid)
      (throw (ex-info "Invalid input" (s/explain-data ::config input)))
      (for [{prop :prop [_ val] :val} parsed]
        (set-config (subs prop 1) val)))))
We can see that "input" is returning from "conform" as "parsed". But it doesn't actually modify the input in any way, unless the input was nonconforming, in which case it returns a special value, which it then explicitly checks for

tjtolton15:03:14

why not just do (if (s/valid? ::config input)?

seancorfield16:03:26

Destructuring plucks out the parts you care about — but leaves all the other parts in place (which you can see with :as).

seancorfield16:03:08

Clojure takes an inherently “open for extension” stance on that sort of thing: it’s considered good practice to be able to accept (and ignore) additions to data structures that still conform.

seancorfield16:03:55

In order to save stuff to a database, you need to explicitly restrict the keys in a map — either with select-keys (ignoring the extra keys) or something that checks you’ve only get the expected keys and no others.

seancorfield16:03:31

In general, when saving to a database (with java.jdbc), we tend to build the map we need from the conformed input map, so we explicitly select the columns we need and ignore the rest. ^ @tjtolton

tjtolton16:03:03

yeah, that's a shame @seancorfield I'm totally down with the "open for extension" concept, but I'm also interested in reducing the cost of detection with spec problems.

tjtolton16:03:53

Basically, permissive specs are great, but they let you do stupid things, like mispelling an optional key

tjtolton16:03:23

if you mispell an optional key, you have no way of knowing about it. Nothing will ever fail it

seancorfield16:03:13

I would expect code that called a function incorrectly like that to be caught by tests on the calling function.

seancorfield16:03:27

i.e., if func-a calls func-b with misspelled arguments, tests for func-a should catch that.

tjtolton17:03:07

so, its interesting, because I basically just want a select-keys, but one that is recursive and spec aware

tjtolton17:03:42

It's still very strange to me that it isn't available. Even in an "open for extension" system, this seems like an important utility.

tjtolton17:03:35

Especially for web programming. I only want to display these values on screen, I only want to return these values in the api call, I only want to save these values to the kv store

tjtolton17:03:41

all things that spec is great at

ikitommi17:03:47

@tjtolton in spec-tools there is a conformer that drops extra keys out of keys-specs. Will blog about those soon.

tjtolton17:03:51

I should be able to reuse that functionality

ikitommi17:03:44

tjtolton: yes, metosin. Just finishing up first release. Comments welcome!

tjtolton17:03:01

awesome, will take a look!

tjtolton17:03:52

in the strip-extra-keys function, what is that first argument? is that something that gets generated by core.spec?

(defn strip-extra-keys [{:keys [:keys pred]} x]
  (if (map? x)
    (s/conform pred (select-keys x keys))
    x))

ikitommi19:03:05

it's the Spec Record. Sadly, clojure.spec/Specs are not extendable and we need to wrap them to make them extendable. There is a example in the readme.

ddellacosta20:03:06

is there a way to enforce that a pair of keys exists in a map, or else neither? That is, if one of them exists the other is required, however they are both optional otherwise.

schmee20:03:20

ddellacosta something like

(fn [m]
  (or (and (:k1 m) (:k2 m)) 
      (or (not (:k1 m)) (not (:k2 m)))))
should do the trick

schmee20:03:30

ie. just use a regular function

ddellacosta20:03:32

ugly but I guess it works. I’ll have to compose it with s/keys so I can enforce the presence of other keys

ddellacosta20:03:58

hmm actually I’m not exactly sure how I would

schmee20:03:22

(s/and (s/keys …) key-check-fn)

ddellacosta20:03:37

I think the second term should be (not (or (:k1 m)) (or (:k2 m)))

dergutemoritz20:03:03

@ddellacosta (s/keys :req [(and ::foo ::bar)])

ddellacosta20:03:30

huh, that works? neat

dergutemoritz20:03:43

Yep, check the doc string

ddellacosta20:03:47

oh but wait, that means they are both required always, doesn’t it?

dergutemoritz20:03:07

Ah yeah, maybe you have to put it in :opt

ddellacosta20:03:31

oh but if I can do that then still, problem solved

dergutemoritz20:03:20

ah no, it's only supported in :req

ddellacosta20:03:55

well, that is lame

ddellacosta20:03:05

but, great idea dergutemoritz , thanks for trying

dergutemoritz20:03:25

Well, good to know in other situations anyway!

ddellacosta20:03:51

in the end this worked:

26│  (s/def ::thing
 27│    (s/and
 28│     (s/keys :req [::required1]
 29│             :opt [::optional1 ::optional2 ::start-date ::end-date])
 30│     (fn [m]
 31│       (or (and (::start-date m) (::end-date m))
 32│           (not (or (::start-date m) (::end-date m)))))))

ddellacosta20:03:13

hopefully including them in the :opt enforces value checking on those fields, going to check now

dergutemoritz20:03:16

Yeah, s/keys checks all keys, even those not specified

dergutemoritz20:03:26

Heh ok that's a bit ambiguous

ddellacosta20:03:26

yeah, I guess I should have assumed that

dergutemoritz20:03:35

Even those not passed to s/keys if they have a spec

ddellacosta20:03:44

but yeah, that works—thanks @schmee and @dergutemoritz for the help

dergutemoritz20:03:51

You're welcome!

ddellacosta20:03:53

oh yeah? didn’t realize that, that’s handy

dergutemoritz20:03:15

Yeah, it's handy but has been the source of surprise in this channel more than once 🙂

ddellacosta20:03:22

I can imagine

dergutemoritz20:03:41

I actually wonder why and and or are not supported in :opt now ...

dergutemoritz21:03:17

Your case makes it seem like a reasonable thing to expect

seancorfield21:03:36

Note that or in :req isn’t mutually exclusive: (s/conform (s/keys :req [(or ::a ::b)]) {::a 1 ::b 2}) succeeds and produces {::b 2 ::a 1}

seancorfield21:03:17

It just means "at least one of these must be present”. Both can be present in the normal “open for extension” model that spec has.

ddellacosta21:03:05

@seancorfield yeah, in this case the problem is that I want these two to either be both present, or not at all

ddellacosta21:03:12

but yeah, good to note

seancorfield21:03:28

Since :opt just means “and here are some more keys you may encounter that have associated specs” — because additional keys are always accepted, they just won’t be assumed to have associated specs to conform to.

ddellacosta21:03:51

right, that makes sense now that I’ve worked through this

seancorfield21:03:21

@ddellacosta Yeah, the best you can do here is specify them both as :opt if you want them conformed, and then s/and a key check predicate around s/keys. We’ve run into this a lot with mutually exclusive keys — we have to have a separate predicate that checks at most one of them is present. Not sure whether I feel this should be built-in or not...

ddellacosta21:03:54

yeah, that’s exactly what I ended up doing. Not the most elegant thing but it works well.

ddellacosta21:03:17

I mean, I feel like @dergutemoritz ‘s suggestion would have done that very elegantly

ddellacosta21:03:29

but maybe that’s challenging to implement, I dunno

ddellacosta21:03:43

haven’t looked at the actual clojure.spec code so dunno

mattly23:03:36

I'm using spec/conform to destructure & validate a largish data structure from a client, and at some point it's using coll-of. But in the process, conform seems to be reversing the contents of the list its given. Is this expected/normal?

Alex Miller (Clojure team)13:03:18

Yes, sounds like that - that’s been fixed (can’t remember which alpha it was in). You might also look at every-kv (which will not conform its input at all).