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

0 votes
in Spec by
{{(clojure.spec/valid? :clojure.spec/any :clojure.spec/invalid)}} returns {{false}}

This issue gets serious, if one likes to write specs for core functions like {{=}} which are used by spec itself. I observed this bug as I wrote a spec for {{assoc}}.

A possible solution could be to use an {{(Object.)}} sentinel internally and {{:clojure.spec/invalid}} only at the API boundary. But I have not thought deeply about this.

9 Answers

0 votes
_Comment made by: akiel_

I have another example were the described issue arises. It's not possible to test the return value of a predicate suitable for conformer, because it should return {{:clojure.spec/invalid}} itself.

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

(s/fdef parse-long
  :args (s/cat :s (s/nilable string?))
  :ret (s/or :val int? :err #{::s/invalid}))

(defn parse-long [s]
    (Long/parseLong s)
    (catch Exception _

0 votes
_Comment made by: akiel_

No change in alpha 10 with the removal of {{:clojure.spec/any}} and introduction of {{any?}}.
0 votes

Comment made by: seancorfield

Another example from Slack, related to this:

(if-let [a 1] ::s/invalid)

Fails compilation (macroexpansion) because {{::s/invalid}} causes the spec for {{if-let}} to think the {{then}} form is non-conforming.


(if-let [a 1] '::s/invalid)

0 votes

Comment made by: ambrosebs

Another example from the wild: https://github.com/pjstadig/humane-test-output/pull/23

A macro rewriting

(is (= ::s/invalid ..))


(let [a ::s/invalid] ...)

resulted in some very strange errors.

0 votes

Comment made by: akiel

The macro issues can be solved by just not using ::s/invalid in code directly. I think in general, it better to use the predicate s/invalid?.

Instead of writing:

(= ::s/invalid ...)

one should use

(s/invalid? ...)

But I have no idea to solve the issue where you have ::s/invalid in data which is validated. The function spec for identical? is a good example.

(s/fdef clojure.core/identical? :args (s/cat :x any? :y any?) :ret boolean?)

world not work.

0 votes

Comment made by: jcr

Please use sumtypes instead of "magic" values to indicate failure or success. For example,

(s/conform any? ::s/invalid) ;=> [:ok ::s/invalid] (s/conform int? ::s/invalid) ;=> [:failure #::s{:problems ... :spec ... :value ...}]

Note that the return value should be an instance of clojure.lang.MapEntry, in order for {{key}} and {{val}} to work on it. However, if it's not desirable to return the explain-map on failure then returning vectors {{[:ok value]}} and {{[:failure]}} (no second element) would work as well.

Since spec is explicitly in alpha, it's not too late yet to fix the api.

Related: CLJ-2115

0 votes
_Comment made by: borkdude_

This is also a problem in tooling REPL like Cursive's. Repro:

(ns assoc.core
   [clojure.spec.alpha :as s]
   [clojure.spec.test.alpha :as stest]))

(s/fdef clojure.core/assoc
  :args (s/cat :map (s/nilable associative?)
               :key any? :val any? :kvs (s/* (s/cat :ks any? :vs any?)))
  :ret associative?)

(stest/instrument `assoc)

(s/conform string? 1)

Evaluating this namespace in a Cursive REPL gives:

Error printing return value (ExceptionInfo) at clojure.spec.test.alpha/spec-checking-fn$conform! (alpha.clj:132).
Call to #'clojure.core/assoc did not conform to spec.

The problem can also reproduced without Cursive by evaluating the above and this:

(defn tooling-repl
  ([] (tooling-repl {}))
   (let [l (read-line)
         evaled (eval (read-string l))]
     (prn evaled)
     (recur (assoc state
                   :*1 evaled
                   :*2 (get state :*1))))))

;; now type in the REPL: (s/conform string? 1)
(with-redefs [read-line (constantly "(s/conform string? 1)")]
0 votes

Comment made by: borkdude

I modified the {{::any}} spec in speculative as follows:

(s/def ::any

(s/conformer #(if (s/invalid? %) ::invalid %))
#(s/gen any?)))


0 votes
Reference: https://clojure.atlassian.net/browse/CLJ-1966 (reported by akiel)