Reworking :pre condition to add an error message

722 views
Skip to first unread message

Timothy Washington

unread,
Jul 11, 2011, 1:40:48 PM7/11/11
to clo...@googlegroups.com
Note: This message was originally posted by ' Shantanu' on the "Re: Clojure for large programs" thread. 

I took a look at  Shantanu's macros, and I like the concept a lot. But I would prefer something baked into the :pre condition itself. The reason is that it just removes a layer of indirection. If you dig into 'clj/clojure/core.clj', you can see that the 'fn' macro is using 'assert' to test these conditions. Assert allows error messages to be applied, ie: 

user => (assert false) 

user => (assert false "fubar") 



However, (defmacro fn ...) assumes that just the boolean condition is being passed in, A). But I'd like to have the option to pass in a message B). 


A) 

(def fubar 

  (fn []

    {:pre [ (true? false) ] }

    (println "Hello World")))

(fubar)


B) 

(def thing 

  (fn []

    {:pre [ [(true? false) "A false message"] ] }

    (println "Hello World")))

(thing)



I reworked the 'fn' macro, only for the :pre condition, as a demonstration (see here). The calling semantics don't change that much. Is there any interest in putting this into core? I'd use Shantanu's workaround otherwise, or in the interim. 

Thanks 

Tim Washington 



On Sun, Jul 3, 2011 at 11:42 AM, Shantanu Kumar <kumar.s...@gmail.com> wrote:


On Jul 3, 7:39 pm, Timothy Washington <twash...@gmail.com> wrote:
> I'm using pre / post assertions quite a bit in a project I'm building. And I
> too would love to see better or custom error messages for each assertion.

That should be possible with a macro. For example, I use this:
https://bitbucket.org/kumarshantanu/clj-miscutil/src/acfb97c662d9/src/main/clj/org/bituf/clj_miscutil.clj#cl-1009

Maybe you need something like this(?):

(defmacro verify-my-arg
 "Like assert, except for the following differences:
 1. does not check for *assert* flag
 2. throws IllegalArgumentException"
 [err-msg arg]
 `(if ~arg true
    (throw (IllegalArgumentException. ~err-msg))))

Then use it thus:

(defn foo [m]
 {:pre [(verify-my-arg "m must be a map" (map? m))]}
 (println m))

Regards,
Shantanu

--
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

Ben Mabey

unread,
Jul 11, 2011, 1:52:08 PM7/11/11
to clo...@googlegroups.com
On 7/11/11 11:40 AM, Timothy Washington wrote:
Note: This message was originally posted by '  Shantanu' on the "Re: Clojure for large programs" thread. 

I took a look at   Shantanu's macros, and I like the concept a lot. But I would prefer something baked into the :pre condition itself. The reason is that it just removes a layer of indirection. If you dig into 'clj/clojure/core.clj', you can see that the 'fn' macro is using 'assert' to test these conditions. Assert allows error messages to be applied, ie: 

user => (assert false) 

user => (assert false "fubar") 



However, (defmacro fn ...) assumes that just the boolean condition is being passed in, A). But I'd like to have the option to pass in a message B). 


A) 

(def fubar 

  (fn []

    {:pre [ (true? false) ] }

    (println "Hello World")))

(fubar)


B) 

(def thing 

  (fn []

    {:pre [ [(true? false) "A false message"] ] }

    (println "Hello World")))

(thing)



I reworked the 'fn' macro, only for the :pre condition, as a demonstration (see here). The calling semantics don't change that much. Is there any interest in putting this into core? I'd use Shantanu's workaround otherwise, or in the interim. 

Thanks 

Tim Washington 


I'd like to see something like this added as well since I sometimes write a comment by conditions to help remind myself of why they are present. One comment on the patch that is probably obvious, but if we change the API for :pre we would want to update the :post API as well to keep them consistent.

-Ben

Timothy Washington

unread,
Jul 11, 2011, 2:01:54 PM7/11/11
to clo...@googlegroups.com
Yes, the :post is easily done as well. I just wanted to throw up a quick proof-of-concept.

Tim


--

Laurent PETIT

unread,
Jul 11, 2011, 2:45:02 PM7/11/11
to clo...@googlegroups.com
If this could integrate with existing efforts put on validation libraries, plus (optionnally) a way to customize the "rendering" of pre-condition errors, this could be great :-)

Maybe just inverting things : if the precondition returns falsy, this could mean that there is no violated precondition. If the precondition returns truethy, this could be printed.

2011/7/11 Timothy Washington <twas...@gmail.com>

Shantanu Kumar

unread,
Jul 12, 2011, 6:01:21 AM7/12/11
to Clojure
As I am the culprit of having introduced it with a naive example, I'd
better admit it may not be very useful in practical scenarios across a
wide variety of use cases. For example, when there is an assertion
error with message "`m` should be a map" 14 levels down the stack, I'd
really wish it said "`m` -- Expected: map, Found: vector [:foo :bar]"
so that I can debug it quickly.

Pre-conditions and Post-conditions are a valuable debugging aid, and
to enable that we need very precise information. Unfortunately passing
a string error message cannot encapsulate enough error context. A more
complex example can be where the correctness of input must be
determined collectively (in association with other args) -- in those
cases one can only fall back on comparing input values and raise
IllegalArgumentException accordingly.

Regards,
Shantanu

On Jul 11, 10:40 pm, Timothy Washington <twash...@gmail.com> wrote:
> Note: This message was originally posted by ' Shantanu' on the "*Re: Clojure
> for large programs*" thread.
>
> I took a look at  Shantanu's macros, and I like the concept a lot. But I
> would prefer something baked into the :pre condition itself. The reason is
> that it just removes a layer of indirection. If you dig into '*
> clj/clojure/core.clj*', you can see that the 'fn' macro is using 'assert' to
> test these conditions. Assert allows error messages to be applied, ie:
>
>    *user => (assert false) *
>
> *user => (assert false "fubar") *
>
> However, (defmacro fn ...) assumes that just the boolean condition is being
> passed in, A). But I'd like to have the option to pass in a message B).
>
> *A) *
>
> **
>
> *(def fubar *
>
> *  (fn []*
>
> *    {:pre [ (true? false) ] }*
>
> *    (println "Hello World")))*
>
> *(fubar)*
>
> *
> *
>
> *B) *
>
>    *(def thing *
>
> *  (fn []*
>
> *    {:pre [ [(true? false) "A false message"] ] }*
>
> *    (println "Hello World")))*
>
> *(thing)*
>
> I reworked the 'fn' macro, only for the :pre condition, as a demonstration (see
> here <http://pastebin.com/fETV1ejJ>). The calling semantics don't change
> that much. Is there any interest in putting this into core? I'd
> use Shantanu's workaround otherwise, or in the interim.
>
> Thanks
>
> Tim Washington
> twash...@gmail.com
> 416.843.9060
>
> On Sun, Jul 3, 2011 at 11:42 AM, Shantanu Kumar <kumar.shant...@gmail.com>wrote:
>
>
>
>
>
>
>
>
>
> > On Jul 3, 7:39 pm, Timothy Washington <twash...@gmail.com> wrote:
> > > I'm using pre / post assertions quite a bit in a project I'm building.
> > And I
> > > too would love to see better or custom error messages for each assertion.
>
> > That should be possible with a macro. For example, I use this:
>
> >https://bitbucket.org/kumarshantanu/clj-miscutil/src/acfb97c662d9/src...

Timothy Washington

unread,
Jul 13, 2011, 11:25:16 AM7/13/11
to clo...@googlegroups.com

I do think a simple String error message is all that the user of the function should provide. From there, An AssertionError can throw up something along the lines of what you said - Expected… , Found… , Message. That would give enough information for reporting at least in a test framework. To get more precise information, like you said, that AssertionError could also throw up class/file information, etc. that a debugger could use. I would guard against designing these things to accomodate a context outside of it's execution scope. In the ideal functional world, the input and output are wholly localized. Any Error/Exception thrown can be consumed or chained to give very precise failure reasoning.  


As for how that would fit into the entire exception chain, that's still being thought (see here). There are already a few approaches, and I think this (see here) is the context of how the core team is approaching this problem. 


Cheers 

Tim 

Colin Taylor

unread,
Mar 29, 2016, 5:02:25 PM3/29/16
to Clojure
Would there be interest in a ticket in this? Seems simple enough if (as above) putting the message under the :pre key is acceptable?

Alex Miller

unread,
Mar 29, 2016, 5:19:12 PM3/29/16
to Clojure
(zombie thread back from the dead... :)

I think enhancements on :pre/:post are interesting.

http://dev.clojure.org/jira/browse/CLJ-1817 seems like a good place to work on this.

Francis Avila

unread,
Mar 29, 2016, 5:40:41 PM3/29/16
to Clojure
A wonderful hack I read about somewhere is to just use the clojure.test/is macro, which I now do all the time:

(require '[clojure.test :refer [is]])
=> nil
(defn get-key [m k]
  {:pre [(is (map? m) "m is not a map!")]}
  (m k))
=> #'user/get-key
(get-key [] 0)

FAIL in clojure.lang.PersistentList$EmptyList@1 (form-init8401797809408331100.clj:2)
m is not a map!
expected: (map? m)
  actual: (not (map? []))
AssertionError Assert failed: (is (map? m) "m is not a map!")  user/get-key (form-init8401797809408331100.clj:1)


This is great for repl use, but it does side-effect (the printed error) and doesn't return anything structured. It's suited to development-time human use rather than runtime or machine-use.

I see the potential for a macro which rethrows the assertion errors as something like ex-info exceptions (i.e. something with structured data.) That would fill runtime or machine-uses better (or structured logging?), but I'm not sure that fits with the spirit of pre/post conditions in the first place. After all, these do raise Java AssertionErrors, which are not meant to be recoverable.

i....@anhero.ru

unread,
Mar 30, 2016, 11:58:21 AM3/30/16
to Clojure
CLJ-1817 looks interesting. We’ve been achieving similar behaviour via `or` statements in `:pre/:post`:

(defn somefn [x]
 
{:pre [(or (map? x) (throw+ {:type ::bad-stuff :details ...}))]}
 
...)


This way we were also getting more useful exception types than AssertionErrors, but it did feel a bit hacky.

Niels van Klaveren

unread,
Mar 30, 2016, 12:09:03 PM3/30/16
to Clojure
Truss also has good support for :pre and :post conditions

Oliver George

unread,
Mar 30, 2016, 10:05:27 PM3/30/16
to Clojure
I look forward to pre/post conditions becoming more helpful.  Truss is a good example of how things can improve.  I think part of the challenge is not making the code too messy.

Here's a proof of concept of how metadata in the pre/post/assert expression could be used to craft nice messages:

(defn get-highlander [hs] 
  {:pre [^{:msg "There can be only one"} (= 1 (count ^:var hs))]} 
  (first hs))

And the error messages can be presented as:

user=> (get-highlander ["asdf" "ss"])
AssertionError Assert failed: There can be only one
(= 1 (count hs))
where hs is ["asdf" "ss"]  user/get-highlander (NO_SOURCE_FILE:4)

This works by modifying the assert macro so it works for general asserts too

(assert (test (complex (form (with (a ^:var varible))))) "bad variable")

The modifications to assert seem quite modest but I'm unsure if this is an approach which is considered to be the correct solution.

(declare tree-seq)

(defn pr-vars [form env]
(let [var? (fn [x] (-> x meta :var))]
(for [var (filter var? (tree-seq seq? identity form))]
`(str "\n where " '~var " is " (pr-str ~var)))))

(defmacro assert
"Evaluates expr and throws an exception if it does not evaluate to
logical tru"
{:added "1.0"}
([x]
(when *assert*
`(when-not ~x
(throw (new AssertionError (str "Assert failed: " ~(or (-> x meta :msg) "")
"\n" (pr-str '~x)
~@(pr-vars x &env)))))))
([x message]
(when *assert*
`(when-not ~x
(throw (new AssertionError (str "Assert failed: " ~message
"\n" (pr-str '~x)
~@(pr-vars x &env))))))))

Reply all
Reply to author
Forward
0 new messages