ANN: clojure.contrib.error-kit

134 views
Skip to first unread message

Chouser

unread,
Feb 6, 2009, 9:10:27 PM2/6/09
to clo...@googlegroups.com
It's not currently possible to define new Java Exceptions without
ahead-of-time compilation, which is sometimes inconvenient. Besides
this, Java's try/catch exception system isn't as flexible as, for
example, Common Lisp's condition/restart system.

To address both these concerns, I've added a lib called error-kit to
clojure-contrib. Its still pretty experimental, so if you find it
awkward to use of think of better names of the lib itself of anything
it defines, please let me know. Of course I'd also love to hear if
anyone finds it useful.

Here's a quick demo of its features:

(require '(clojure.contrib [error-kit :as kit]))

This defines an error and its action if unhandled. A good choice of
unhandled action is to throw a Java exception so users of your code
who do not want to use error-kit can still use normal Java try/catch
forms to handle the error. The throw-msg macro helps you do this:

(kit/deferror *number-error* [] [n]
{:msg (str "Number error: " n)
:unhandled (kit/throw-msg NumberFormatException)})

This next error derives from the one above, which means if
*odd-number-error* is raised, it can be caught by a handler for
*number-error*:

(kit/deferror *odd-number-error* [*number-error*]
"Indicates an odd number was given to an operation that is only
defined for even numbers."
[n]
{:msg (str "Can't handle odd number: " n)})

Raise an error by name with any extra args as defined by the deferror:

(defn int-half [i]
(if (even? i)
(quot i 2)
(kit/raise *odd-number-error* i)))

Since this form ends up calling 'raise', but has no 'with-hander' and
'handle' form, it will throw a regular Java NumberFormatException:

(vec (map int-half [2 4 5 8]))

But you can handle the error, collect details from the args passed to
'raise', and do whatever you want with it. In this case, just throw
an Exception with a more specific message:

(kit/with-handler
(vec (map int-half [2 4 5 8]))
(kit/handle *odd-number-error* [n]
(throw (Exception. (format "Odd number %d in vector." n)))))

The above is equivalent to the more complicated version below:

(kit/with-handler
(vec (map int-half [2 4 5 8]))
(kit/handle {:keys [n tag]}
(if (isa? tag `*odd-number-error*)
(throw (Exception. (format "Odd number %d in vector." n)))
(kit/do-not-handle))))

Returns "invalid" string instead of a vector when an error is
encountered. This is like Clojure's try/catch form.

(kit/with-handler
(vec (map int-half [2 4 5 8]))
(kit/handle kit/*error* [n]
"invalid"))

Inserts a zero into the returned vector where there was an error, in
this case [1 2 0 4]

(kit/with-handler
(vec (map int-half [2 4 5 8]))
(kit/handle *number-error* [n]
(kit/continue-with 0)))

Here's how to use an intermediate continue, like a CL restart. The
with-handler form below will return [1 2 :oops 5 4]

(defn int-half-vec [s]
(reduce (fn [v i]
(kit/with-handler
(conj v (int-half i))
(kit/bind-continue instead-of-half [& instead-seq]
(apply conj v instead-seq))))
[] s))

(kit/with-handler
(int-half-vec [2 4 5 8])
(kit/handle *number-error* [n]
(kit/continue instead-of-half :oops n)))

A couple questions, for anyone still reading...

Should continue-names be namespace qualified, and therefore require
pre-definition in some namespace, like this?

(kit/defcontinue skip-thing "docstring")

Could add 'catch' for Java Exceptions and 'finally' support to
with-handler forms.

--Chouser

David Nolen

unread,
Feb 7, 2009, 1:45:17 PM2/7/09
to clo...@googlegroups.com
Nice I will most definitely be checking this out and get back with thoughts.  I've been fearing the need to define custom Java Exception for my code and this looks most useful for people who wish to code as much as possible in pure Clojure.

David Nolen

unread,
Feb 8, 2009, 5:18:13 AM2/8/09
to clojure
So far this is fantastic! And I haven't even got around to playing with the restart mechanism :) I'm using it in Spinoza to provide contextual errors (malformed slot list, missing base class etc.).  I notice the contrib libraries in general are very good at throwing exceptions so that consumers have some idea of what is going on.  error-kit one ups this practice by providing a standardized way to define your errors up front and presents a definition system that removes the need to instantiate ugly Java Exceptions inline with your code.

(defn protocol-fn [protocol-name fn-name]
  (let [protocol-keyword (symbol-to-keyword protocol-name)
dispatch (partial protocol-dispatch protocol-keyword)]
    `(do 
       (defmulti ~fn-name ~dispatch)
       (defmethod ~fn-name [:spinoza/object nil]
[~'obj]
(raise *missing-protocol-method-error*
~'obj ~(str protocol-name) ~(str fn-name))))))

Where the error is defined as you've described:

(deferror *missing-protocol-method-error* [*spinoza-error*]
 [obj protocol-name method-name]
 {:msg (str (:tag obj)
    " class does not implement "
    method-name
    " from protocol " 
    protocol-name)})

Sweet!

That said I do have one minor annoyance and that is the need to leave an empty bracket if you want to create a new error without inheriting from a previously defined error.  Very minor.  Will provide feedback on restarts when I get there.

Chouser

unread,
Feb 8, 2009, 8:31:23 AM2/8/09
to clo...@googlegroups.com
On Sun, Feb 8, 2009 at 5:18 AM, David Nolen <dnolen...@gmail.com> wrote:
>
> Sweet!

I'm glad it's working for you, and that you have figured out how to
use it despite the almost complete lack of docs. :-P

> That said I do have one minor annoyance and that is the need to leave an
> empty bracket if you want to create a new error without inheriting from a
> previously defined error. Very minor.

This was actually intentional. I had a pre-release version where
those brackets were optional, but the doc-string could still be
specified. But I was worried this would be too confusing since it
meant that if you then inserted the parent vector, that would have to
come before the doc-string, but the args vector would have to be
inserted after.

(deferror *foo* "Foo Error")
(deferror *foo* [*bar*] "Foo Error")
(deferror *foo* "Foo Error" [arg1])
(deferror *foo* [*bar*] "Foo Error" [arg1])

And perhaps worst, if you got it wrong, I may not be able to detect
the mistake:

(deferror *foo* "Foo Error")
(deferror *foo* "Foo Error" [*bar*])
(deferror *foo* "Foo Error" [*bar*] [arg1]) ; oops

That last line would be using [*bar*] as the arg vector and [arg1] as
the error object definition.

...anyway, that's reasoning behind requiring at least placeholder
empty brackets before the doc-string. But I'm open to being persuaded
otherwise. Perhaps allow no parent vector if there's also no
doc-string?

Oh, and by the way with an empty or missing parent vector, the error
would still be derived from kit/*error*.

> Will provide feedback on restarts when I get there.

Great! But they're called continues. :-) Seriously, if you think
"restart" is a better name I'm willing to consider changing it. But
Clojure already has retries in transactions, where code is actually
re-run from the beginning. A "restart" doesn't re-run anything, it
just skips forward over a certain number of returns and runs an
alternate branch of code. "continue" seemed a better description of
that to me than "restart".

--Chouser

Meikel Brandmeyer

unread,
Feb 8, 2009, 9:24:20 AM2/8/09
to clo...@googlegroups.com
Hi,

Am 08.02.2009 um 14:31 schrieb Chouser:

> (deferror *foo* [*bar*] "Foo Error" [arg1])

> (deferror *foo* "Foo Error" [*bar*] [arg1]) ; oops

A question w/o looking at the actual implementation
code or further thinking about possible issues:

Is there a reason why not to place the docstring
as in the second example?

All the def*s do it like that:

(defn foo "doc-string" [args] ...)
(defmacro foo "doc-string" [args] ...)
(defmulti foo "doc-string" ...)
(ns foo.bar "doc-string" ...)

One exception is defvar from contrib.

Sincerely
Meikel

Chouser

unread,
Feb 8, 2009, 11:39:45 AM2/8/09
to clo...@googlegroups.com
On Sun, Feb 8, 2009 at 9:24 AM, Meikel Brandmeyer <m...@kotka.de> wrote:
> Hi,
>
> Am 08.02.2009 um 14:31 schrieb Chouser:
>
>> (deferror *foo* [*bar*] "Foo Error" [arg1])
>> (deferror *foo* "Foo Error" [*bar*] [arg1]) ; oops
>
> A question w/o looking at the actual implementation
> code or further thinking about possible issues:
>
> Is there a reason why not to place the docstring
> as in the second example?
>
> All the def*s do it like that:
>
> (defn foo "doc-string" [args] ...)
> (defmacro foo "doc-string" [args] ...)
> (defmulti foo "doc-string" ...)
> (ns foo.bar "doc-string" ...)

In all those examples, the args (if any) are immediately after the
doc-string, and deferror does the same. It would theoretically be
possible for deferror to provide multiple bodies for different
arities, just like defn, so using the same order makes sense.

--Chouser

David Nolen

unread,
Feb 15, 2009, 10:50:28 PM2/15/09
to clo...@googlegroups.com
Would it be possible to make the arguments to handle be optional? Is this a good or bad idea?

It seems to me, in the case of setting up test fixtures that check for raised errors, often you don't care what arguments the error takes.

David

On Fri, Feb 6, 2009 at 9:10 PM, Chouser <cho...@gmail.com> wrote:

Chouser

unread,
Feb 15, 2009, 11:29:06 PM2/15/09
to clo...@googlegroups.com
On Sun, Feb 15, 2009 at 10:50 PM, David Nolen <dnolen...@gmail.com> wrote:
> Would it be possible to make the arguments to handle be optional? Is this a
> good or bad idea?

(kit/with-handler


(vec (map int-half [2 4 5 8]))
(kit/handle *odd-number-error* [n]
(throw (Exception. (format "Odd number %d in vector." n)))))

The args vector ([n] above) is actually a vector in a :keys
destructuring expression, so its elements are option. In fact map
being destructured has several other bit of information that may be
useful. You can see them like this:

(kit/with-handler
(vec (map int-half [2 4 5 8]))

(kit/handle *odd-number-error* {:as err}
err))
-> {:n 5, :tag user/*odd-number-error*,
:msg "Can't handle odd number: 5", :unhandled #<...>}

That map includes values inherited from parent error types. So you
can definitely leave the args vector empty.

I think it might be more confusing that useful to allow leaving out
the args vector entirely. Currently the expressions after the args
vector are expanded into an implicit 'do', so you can have as many as
you'd like. Detecting whether the first expression after the error
name is a vector (or map) and so should be treated as args vs. a list
to be an evaluated expression seems potentially confusing.

--Chouser

Reply all
Reply to author
Forward
0 new messages