Welcome! Please see the About page for a little more info on how this works.

0 votes
in Spec by
user=> (require '[clojure.spec.alpha :as s])
user=> (s/def ::a int?)
user=> (s/def ::b int?)
user=> (s/def ::c (s/merge (s/keys :req [::a ::b]) #(< (::a %) (::b %))))
user=> (s/explain-data ::c {::a 0 ::b 1})
#:clojure.spec.alpha{:problems ({:path [], :pred (clojure.core/fn [%] (clojure.core/< (:user/a %) (:user/b %))), :val #:user{:a 0, :b 1}, :via [:user/c], :in []}), :spec :user/c, :value #:user{:a 0, :b 1}}
user=> ;; ^^ not expected
user=> (s/explain-data ::c {::a 1 ::b 0})
#:clojure.spec.alpha{:problems ({:path [], :pred (clojure.core/fn [%] (clojure.core/< (:user/a %) (:user/b %))), :val #:user{:a 1, :b 0}, :via [:user/c], :in []}), :spec :user/c, :value #:user{:a 1, :b 0}}
user=> ;; ^^ expected
user=> (s/def ::a<b #(< (::a %) (::b %)))
user=> (s/def ::d (s/merge (s/keys :req [::a ::b]) ::a<b))
user=> (s/explain-data ::d {::a 0 ::b 1})
user=> (s/explain-data ::d {::a 1 ::b 0})
#:clojure.spec.alpha{:problems ({:path [], :pred (clojure.core/fn [%] (clojure.core/< (:user/a %) (:user/b %))), :val #:user{:a 1, :b 0}, :via [:user/d :user/a<b], :in []}), :spec :user/d, :value #:user{:a 1, :b 0}}

10 Answers

0 votes
_Comment made by: dchelimsky_

It also always fails when referring to a predicate function, so the only way to get it to work is to define another spec and refer to that.

user=> (defn a<b [a b] (< a b))
user=> (s/def ::e (s/merge (s/keys :req [::a ::b]) a<b))
user=> (s/explain-data ::e {::a 1 ::b 2})
#:clojure.spec.alpha{:problems ({:path [], :pred user/a<b, :val #:user{:a 1, :b 2}, :via [:user/e], :in []}), :spec :user/e, :value #:user{:a 1, :b 2}}
user=> (s/explain-data ::e {::a 1 ::b 0})
#:clojure.spec.alpha{:problems ({:path [], :pred user/a<b, :val #:user{:a 1, :b 0}, :via [:user/e], :in []}), :spec :user/e, :value #:user{:a 1, :b 0}}
0 votes

Comment made by: alexmiller

(< (::a % ::b %)) doesn't seem right? Does #(< (::a %) (::b %)) work?

0 votes

Comment made by: dchelimsky

Good catch, however, no #(< (::a %) (::b %)) does not work inline. I updated the example with to reflect that.

0 votes

Comment made by: dchelimsky

Also, FWIW, I'd narrowed down the problem elsewhere, before my typo :) Just so happened that I got the same answer with and without the typo.

0 votes
_Comment made by: dchelimsky_

It does work if you wrap the predicate in {s/spec}:

user=> (s/def ::f (s/merge (s/keys :req [::a ::b]) (s/spec #(< (::a %) (::b %)))))
user=> (s/explain-data ::f {::a 1 ::b 2})
user=> (s/explain-data ::f {::a 1 ::b 0})
#:clojure.spec.alpha{:problems ({:path [], :pred (clojure.core/fn [%] (clojure.core/< (:user/a %) (:user/b %))), :val #:user{:a 1, :b 0}, :via [:user/f], :in []}), :spec :user/f, :value #:user{:a 1, :b 0}}
0 votes
_Comment made by: jcr_

I'm unable to reproduce any of this.

In the issue description, the predicate {{#(< (::a %) (::b %))}} require a to be less than b, so it is expected that {{{::a 1 ::b 0}}} doesn't match the spec.

In the first comment, the predicate {{a<b}} is invalid, since it accepts two numbers instead of a single map.

The following snippet works as expected too (there's no difference between anonymous pred, named pred and a pred wrapped in s/spec):

(s/def ::a number?)
(defn good? [m] (contains? m :a))

(def s1 (s/merge (s/keys :req-un [::a]) good?))
(def s2 (s/merge (s/keys :req-un [::a]) #(contains? % :a)))
(def s3 (s/merge (s/keys :req-un [::a]) (s/spec #(contains? % :a))))

(s/valid? s1 {:a 42}) ;=> true
(s/valid? s2 {:a 42}) ;=> true
(s/valid? s3 {:a 42}) ;=> true

I believe the issue can be closed.
0 votes

Comment made by: dchelimsky

When I updated the example after Alex's comment, I put the message in the wrong place. I just updated it again with the messages correctly aligning w/ the output. jcr, I do see that your examples work, but mine still do not. So this is not quite as general as all anonymous predicates, but there is still surprising behavior.

That said, s/merge docs say it supports keys specs, and does not claim to support any other type of predicate, so if you want to close it on those grounds, have at it.

0 votes
_Comment made by: jcr_

Ah, now I see what you mean. Sorry for jumping to conclusions.

So the actual problem here is that {{s/explain}} inconsistently reports inputs that are valid according to {{s/conform}} when {{s/merge}} is used with a predicate not wrapped in a {{s/spec}} call. Here's the minimal example I've managed to come up with:

(ns test
  (:require [clojure.spec.alpha :as s]))

(defn pred [m] (contains? m :a))
(s/def ::s (s/merge pred))

(def good {:a 42})
(def bad  {})

;; as expected
(s/conform ::s good) ;=> {:a 42}
(s/conform ::s bad)  ;=> :clojure.spec.alpha/invalid

;; !!!: first one should NOT fail
(s/explain-str ::s good) ;=> "val: {:a 42} fails spec: :test/s predicate: pred\n"
(s/explain-str ::s bad)  ;=> "val: {} fails spec: :test/s predicate: pred\n"

;; explicitly wrap pred in spec
(s/def ::s* (s/merge (s/spec pred)))

;; now, works as expected
(s/explain-str ::s* good) ;=> "Success!\n"
(s/explain-str ::s* bad)  ;=> "val: {} fails spec: :test/s* predicate: pred\n"

I would suggest updating the title and the description accordingly, if you don't mind?
0 votes
_Comment made by: jcr_

As far as I can tell, the problem is that {{merge-spec-impl}} simply calls {{explain-1}} with the given preds, and {{explain-1}} always returns problems if {{(spec? pred)}} is false; merge-spec-impl should probably call {{specize}} on its preds, similar to how it's done in the {{tuple-impl}}:

(let [specs (delay (mapv specize preds forms)) ...] ...)
0 votes
Reference: https://clojure.atlassian.net/browse/CLJ-2321 (reported by dchelimsky)