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