How can I get Clojure :pre & :post to report their failing value?
@octopusgrabbus kind of hinted at this by proposing (try ... (catch ...))
, and you mentioned that that might be too noisy, and is still wrapped in an assert. A simpler and less noisy variant of this would be a simple (or (condition-here) (throw-exception-with-custom-message))
syntax, like this:
(defn string-to-string [s1]
{:pre [(or (string? s1)
(throw (Exception. (format "Pre-condition failed; %s is not a string." s1))))]
:post [(or (string? %)
(throw (Exception. (format "Post-condition failed; %s is not a string." %))))]}
s1)
This essentially lets you use pre- and post-conditions with custom error messages -- the pre- and post-conditions are still checked like they normally would be, but your custom exception is evaluated (and thus thrown) before the AssertionError can happen.
Something like below where clojure spec is explaining the problem? This will throw an assertion error which you can catch.
(defn string-to-string [s1]
{:pre [ (or (s/valid? ::ur-spec-or-predicate s1)
(s/explain ::ur-spec-or-predicate s1)]}
s1)
Clojure spec can be used to assert on arguments, yielding an exception on invalid input with data explaining why the failure occurred (assertion checking has to be turned on):
(require '[clojure.spec.alpha :as s])
;; "By default assertion checking is off - this can be changed at the REPL
;; with s/check-asserts or on startup by setting the system property
;; clojure.spec.check-asserts=true"
;;
;; quoted from https://clojure.org/guides/spec#_using_spec_for_validation
(s/check-asserts true)
(defn string-to-string [s1]
{:pre [(s/assert string? s1)]
:post [(s/assert string? %)]}
s1)
(string-to-string nil) => #error{:cause "Spec assertion failed\nnil - failed: string?\n",
:data #:clojure.spec.alpha{:problems [{:path [], :pred clojure.core/string?, :val nil, :via [], :in []}],
:spec #object[clojure.core$string_QMARK___5395 0x677b8e13 "clojure.core$string_QMARK___5395@677b8e13"],
:value nil,
:failure :assertion-failed}}
The [:data :value]
key in the exception shows you the failing value. The [:data :problems]
key shows you why spec thinks the value is invalid. (In this example the problem is straightfoward, but this explanation gets very useful when you have nested maps and multiple specs composed together.)
One important caveat is that s/assert
when given valid input returns that input, yet the :pre
and :post
conditions check for truthiness. If the validation conditions you need consider falsy values to be valid, then you need to adjust your validation expression, otherwise s/assert
will succeed, but the truthiness check in :pre
or :post
will fail.
(defn string-or-nil-to-string [s1]
{:pre [(s/assert (s/or :string string? :nil nil?) s1)]
:post [(s/assert string? %)]}
(str s1))
(string-or-nil-to-string nil) => AssertionError
Here's what I use to avoid that problem:
(defn string-or-nil-to-string [s1]
{:pre [(do (s/assert (s/or :string string? :nil nil?) s1) true)]
:post [(s/assert string? %)]}
(str s1))
(string-or-nil-to-string nil) => ""
Edit: enable assertion checking.
You could wrap your predicate with the is
macro from clojure.test
(defn string-to-string [s1]
{:pre [(is (string? s1))]
:post [(is (string? %))]}
s1)
Then you get:
(string-to-string 10)
;FAIL in clojure.lang.PersistentList$EmptyList@1 (scratch.clj:5)
;expected: (string? s1)
;actual: (not (string? 10))