clojure.spec - Using :pre conditions (or not)?

2,470 views
Skip to first unread message

joakim.t...@nova.com

unread,
Sep 14, 2016, 8:37:24 AM9/14/16
to Clojure
(ns spec-test.core
(:require [clojure.spec :as s]))

(s/def :user/name string?)
(s/def :common/user (s/keys :req [:user/name]))

; first version of name (using :pre)
(defn name [user]
{:pre [(s/valid? :common/user user)]}
(-> user :user/name))

; This statement works ok and returns "Elon":
(name {:user/name "Elon"})

; but this statement...
(name {:x "Elon"})

;...will throw:
CompilerException java.lang.AssertionError:
Assert failed: (s/valid? :common/user user)

; ...but then I don't get as much information
; about the error as if I would have called:
(s/explain :common/user {:x "Elon"})

;...which also contains the predicate:
val: {:x "Elon"} fails spec: :common/user
predicate: (contains? % :user/name)

; (second version of name - more verbose)
; or do I need to wite it like this:
(defn name [user]
(let [parsed (s/conform :common/user user)]
(if (= parsed ::s/invalid)
(throw (ex-info "Invalid input" (s/explain-data :common/user user)))
(-> user :user/name))))

; so that:
(name {:x "Elon"})

; ...will return:
CompilerException clojure.lang.ExceptionInfo:
Invalid input #:clojure.spec{:problems}
({:path [], :pred (contains? % :user/name),
:val {:x "Elon"}, :via [:common/user], :in []})

; It should be nice if I could be able to write it like this
; (or similar, to get a better error message):
(defn name [user]
{:pre [(s/explain :common/user user)]}
(-> user :user/name))

Alex Miller

unread,
Sep 14, 2016, 9:59:09 AM9/14/16
to Clojure
Another option that has been added since the guide was written is s/assert which seems closer to what you're suggesting.

(defn name [user]
  {:pre [(s/assert :common/user user)]}
  (-> user :user/name))

;; need to enable assertion checking - this can also be enabled globally with system property clojure.spec.check-asserts
(s/check-asserts true)

(name {:user/name "Elon"})
"Elon"

(name {:x "Elon"})
ExceptionInfo Spec assertion failed
val: {:x "Elon"} fails predicate: (contains? % :user/name)
:clojure.spec/failure  :assertion-failed
  clojure.core/ex-info (core.clj:4725)

Rather than use it in a precondition, you can also use s/assert directly in the code.

joakim.t...@nova.com

unread,
Sep 15, 2016, 7:20:17 AM9/15/16
to Clojure
Ok, thanks!

In the Java world, the assertions is also something that need to be turn on explicitly.
In that sence, they are kind of not mandatory to be executed (or at least signals that to the reader of the code).

I would be happier if you guys could add another method, that I can use in my :pre conditions, that leverage
the same amount of details in the error messages, but that is always "turned on".

In the meanwhile, I will use s/assert ;-)

BR,
Joakim Tengstrand

Shantanu Kumar

unread,
Sep 15, 2016, 9:11:32 AM9/15/16
to Clojure
Hi Joakim,

You might be interested in Paul Stadig's library https://github.com/pjstadig/assertions that leverages Java's `-ea` (enable-assertions, which you may want to keep enabled in dev) command-line flag. If you have a bunch of things together to assert, you may want to use the `when-assert` macro for wholesale optimization: https://github.com/pjstadig/assertions/blob/0.2.0/src/pjstadig/assertions.clj#L13


Shantanu

joakim.t...@nova.com

unread,
Sep 15, 2016, 10:13:09 AM9/15/16
to Clojure
Thanks for the tip!

joakim.t...@nova.com

unread,
Sep 16, 2016, 1:20:53 AM9/16/16
to Clojure
I came up with this solution:

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

(s/def :user/name string?)
(s/def :common/user (s/keys :req [:user/name]))

;; with this little helper function...
(defn check [type data]
(if (s/valid? type data)
true
(throw (AssertionError. (s/explain type data)))))

;; I can use it in my :pre condition
(defn aname [user]
{:pre [(check :common/user user)]}
(-> user :user/name))

;; when I call name with an illegal arguement...
(aname {:x "Elon"})

;; ...it not fails and returns a better error message:
CompilerException java.lang.AssertionError: null, compiling:(/Users/joakimtengstrand/IdeaProjects/spec-test/src/spec_test/core.clj:19:1)

val: {:x "Elon"} fails spec: :common/user predicate: (contains? % :user/name)

With this solution I don't need to enable assertions, and the code is neat and less verbose!

/Joakim

On Thursday, September 15, 2016 at 3:11:32 PM UTC+2, Shantanu Kumar wrote:

Mamun

unread,
Sep 16, 2016, 1:52:55 AM9/16/16
to Clojure

When function is throwing exception because of argument. I would prefer to throw IllegalArgumentException not AssertionError. 


(defn check [type data]
  (if (sp/valid? type data)
    true
    (throw (IllegalArgumentException. (sp/explain type data)))))


Br,
Mamun

joakim.t...@nova.com

unread,
Sep 16, 2016, 4:20:26 AM9/16/16
to Clojure
I agree with you that an IllegalArgumentException is preferable to AssertionError.
The reason that I used AssertionError was that I wanted to keep the same behaviour as when using s/valid? (it throws an AssertionError).
Maybe only s/assert should throw AssertionError and s/valid? should throw something else?

/Joakim

David Goldfarb

unread,
Jun 7, 2017, 9:12:22 AM6/7/17
to Clojure
One big downside of using s/assert in a precondition:  It does not work with (s/nilable ...) specs, since s/assert returns valid values.

I fell into this trap for a moment of head-scratching just now.

Alex Miller

unread,
Jun 7, 2017, 10:46:23 AM6/7/17
to Clojure
Preconditions are already assertions, so it makes more sense to use s/assert in your code body than in a precondition.

David Goldfarb

unread,
Jun 8, 2017, 3:41:14 AM6/8/17
to clo...@googlegroups.com

Agreed; I was just following up on your previous comments.

 

But, it is useful to have something that can be used in a precondition and also shows an explanation.

 

I’m using, basically, (or (s/valid? spec x) (s/explain spec x)). It would be good to have this built-in too.

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to a topic in the Google Groups "Clojure" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/clojure/H9tk04sSTWE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages