Towards an integrated error handling for clojure (or: clojure.contrib.condition suggestion)

176 views
Skip to first unread message

Meikel Brandmeyer

unread,
Nov 11, 2010, 5:33:37 AM11/11/10
to Clojure Dev
Hi,

I think there was some discussion about improving on c.c.condition. If
we do this anyway, I'd like to propose a replacement for handler-case
which integrates better with the host exception system.

Here the code for a possible try-case form.

(defmacro try-case
[dispatch-fn & body]
(let [[code handlers finally]
(loop [body (seq body)
code []
handlers []
finally nil]
(if body
(let [form (first body)]
(cond
(not (seq? form))
(recur (next body) (conj code form) handlers finally)
(= 'catch (first form))
(recur (next body) code (conj handlers form) finally)
(= 'finally (first form))
(recur (next body) code handlers form)
:else
(recur (next body) (conj code form) handlers
finally)))
[code handlers finally]))]
`(try
~@code
~@(when-let [handlers (seq handlers)]
[`(catch Throwable t#
(let [condition# (if (instance? Condition t#) (meta t#)
t#)]
(condp #(isa? %2 %1) (~dispatch-fn t#)
~@(mapcat
(fn [[_catch key exc & body]]
`[~key (let [~exc condition#] ~@body)])
handlers)
:else (raise t#))))])
~@(when finally [finally]))))

The idea is to dispatch on the exception, not the metadata of the
Condition. That way we could use for example type to get a nice
integration with the host system. Since Conditions carry metadata
there :type field would be inspected, while normal exception are
handled via class. Similar to multimethods you are then free to define
your own hierarchy of errors if desired.

Here a silly example demonstrating the form of try-case. As you see
it's quite similar to a normal try.

(try-case type
foo
(bar)
baz
(catch ::MyError my-err
(frob my-err))
(catch Exception e
foo
bar)
(finally
(cleanup)))

And it's expansion (edited for readability):

(try
foo
(bar)
baz
(catch java.lang.Throwable t__35__auto__
(let [condition__36__auto__ (if (instance? Condition
t__35__auto__)
(meta t__35__auto__)
t__35__auto__)]
(condp (fn* [p1__33__37__auto__ p2__32__38__auto__]
(isa? p2__32__38__auto__ p1__33__37__auto__))
(type t__35__auto__)
:user/MyError (let [my-err condition__34__auto__]
(frob my-err))
Exception (let [e condition__34__auto__]
foo
bar)
:else (raise t__35__auto__))))
(finally
(cleanup)))

As a side effect I removed the global variables which are set via
binding. That was named as a restriction somewhere.

What do you think?

Sincerely
Meikel

Meikel Brandmeyer

unread,
Nov 11, 2010, 5:57:27 AM11/11/10
to Clojure Dev
Hi,

On 11 Nov., 11:33, Meikel Brandmeyer <m...@kotka.de> wrote:

>                    :else (raise t#))))])

Oops. Minus this :else since we use condp...

Sincerely
Meikel

Rasmus Svensson

unread,
Nov 11, 2010, 7:04:10 AM11/11/10
to cloju...@googlegroups.com
2010/11/11 Meikel Brandmeyer <m...@kotka.de>:

> Hi,
>
> I think there was some discussion about improving on c.c.condition. If
> we do this anyway, I'd like to propose a replacement for handler-case
> which integrates better with the host exception system.

I like this very much. Having a unified exception system that can
interact with both typed exceptions from the JVM world and with
ordinary maps and keywords from the Clojure world is very desirable,
IMHO. I have some questions:

1) What would a typical dispatch fn look like? From the above code, I
can read that it will always be passed the throwable object. So I
guess it will always have to call meta on its argument to get the data
contained in the Condition (or nil, if it some other kind of
throwable).

I think that handling both arbitrary throwables and Conditions in the
same try-case makes most sense when using type as the dispatch fn, but
I guess nothing stops you from making another dispatch fn that uses
some part of the metadata and falls back to the class of the
throwable.

I think my concern is that writing dispatch fns should be easy. Fns
like #(meta (:some-key %)) or #(or (meta (:some-key %)) (class %)) are
perhaps easy enough to write.

Another way would perhaps be to use the class of the throwable as the
dispatch value if the dispatch fn returns nil. Maybe the fn should be
passed the metadata in this case. (This is just an idea I had. I
haven't thought about its implications very thoroughly.)

I like that the "catch variable" is bound to either the metadata or
the throwable, depending on whether the throwable is a Condition or
not. I have a feeling that I would like the dispatch fn to be used in
a similar way.

2) Should the macro look for the catch and finally symbols or vars?

sincerely,
Rasmus

Stuart Halloway

unread,
Nov 11, 2010, 9:11:45 AM11/11/10
to cloju...@googlegroups.com
Please make sure the result of this conversation gets a page at http://dev.clojure.org/display/design.

Stu

> --
> You received this message because you are subscribed to the Google Groups "Clojure Dev" group.
> To post to this group, send email to cloju...@googlegroups.com.
> To unsubscribe from this group, send email to clojure-dev...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/clojure-dev?hl=en.
>

Chouser

unread,
Nov 11, 2010, 9:24:35 AM11/11/10
to cloju...@googlegroups.com
On Thu, Nov 11, 2010 at 5:33 AM, Meikel Brandmeyer <m...@kotka.de> wrote:
> Hi,
>
> I think there was some discussion about improving on c.c.condition. If
> we do this anyway, I'd like to propose a replacement for handler-case
> which integrates better with the host exception system.

This is a worthy goal.

What do you think of using a pattern matching form instead of
dispatching strictly on type? The set of handlers is lexically
closed anyway, so it seems like a good fit in that regard. And
I've heard rumours contrib will be getting a nice pattern
matching library soon...

--Chouser
http://joyofclojure.com/

Meikel Brandmeyer

unread,
Nov 11, 2010, 4:13:14 PM11/11/10
to Clojure Dev
Hi,

On 11 Nov., 15:11, Stuart Halloway <stuart.hallo...@gmail.com> wrote:

> Please make sure the result of this conversation gets a page athttp://dev.clojure.org/display/design.

Here we go: http://dev.clojure.org/display/design/Exception+Handling

Meikel

Meikel Brandmeyer

unread,
Nov 12, 2010, 4:30:04 AM11/12/10
to Clojure Dev
Hi,

and as it turns out there is already a patch moulding in assembla for
7 months. :(

However I have some concerns with the patch:

* It requires two different clause types for Conditions and regulars
Exceptions (handle and catch). Why should we make a difference?
* It does not allow a finally clause.
* It still uses binding. (Really a problem? I remember someone
complaining about it, but I can't find a reference anymore. Speak up
if you have an issue with this!)

Sincerely
Meikel

Phil Hagelberg

unread,
Nov 12, 2010, 2:14:08 PM11/12/10
to cloju...@googlegroups.com
On Fri, Nov 12, 2010 at 1:30 AM, Meikel Brandmeyer <m...@kotka.de> wrote:
> However I have some concerns with the patch:
>
> * It requires two different clause types for Conditions and regulars
> Exceptions (handle and catch). Why should we make a difference?

My initial reaction is that "catch" already means something pretty
specific in Clojure. However, if try-case's catch falls back to
supporting a class when the dispatch function returns nil maybe this
isn't a problem? I don't know if it's worth the complexity of trying
to avoid calling the dispatch function when it's unnecessary;
hopefully the dispatch is always very fast and not called at all in
the normal course of execution.

So in summary: I am fine with collapsing handle and catch.

> * It does not allow a finally clause.

Yes, an oversight.

> * It still uses binding. (Really a problem? I remember someone
> complaining about it, but I can't find a reference anymore. Speak up
> if you have an issue with this!)

I think as long as the things rebound are declared dynamic it
shouldn't be an issue. The dynamicity is well-hidden from the user, as
far as I can tell. But if try-case does the same things without
dynamicity then I suppose that's better.

The other difference between try-case and handler-case is that
try-case is more like the "try" that already exists in Clojure. If
it's going into Clojure itself and it uses the term "catch" in a way
that preserves the semantics of the existing catch then try-case may
be better. I am not particularly attached to my patch, so don't feel
you need to try hard to convince me. =)

If this does go into Clojure I think we should consider making throw
work like raise when you call it with a map argument.

cheers,
Phil

Meikel Brandmeyer

unread,
Nov 22, 2010, 6:30:24 AM11/22/10
to Clojure Dev
Hi,

On 12 Nov., 20:14, Phil Hagelberg <p...@hagelb.org> wrote:

> My initial reaction is that "catch" already means something pretty
> specific in Clojure. However, if try-case's catch falls back to
> supporting a class when the dispatch function returns nil maybe this
> isn't a problem? I don't know if it's worth the complexity of trying
> to avoid calling the dispatch function when it's unnecessary;
> hopefully the dispatch is always very fast and not called at all in
> the normal course of execution.
>
> So in summary: I am fine with collapsing handle and catch.

continueing with

> The other difference between try-case and handler-case is that
> try-case is more like the "try" that already exists in Clojure. If
> it's going into Clojure itself and it uses the term "catch" in a way
> that preserves the semantics of the existing catch then try-case may
> be better. I am not particularly attached to my patch, so don't feel
> you need to try hard to convince me. =)
>
> If this does go into Clojure I think we should consider making throw
> work like raise when you call it with a map argument.

Mulling about this led me to thinking about whether we should keep try/
catch/throw as low-level constructs and add handler-case (formerly
known as try-case in this discussion) on top making handle also take
care of host Exceptions. So existing programs would not be affected
and host interaction is directly possible if necessary.

As far as dispatch is concerned: Is full flexibility needed? Should we
short-circuit to the exception class if it is not a Condition only
call the dispatch fn on the later? Should we provide some helper (on-
condition ...) which does that? What is the common use case and what
should it cost to do it/something else?

Sincerely
Meikel

Rasmus Svensson

unread,
Dec 16, 2010, 1:30:28 PM12/16/10
to cloju...@googlegroups.com
I have though a bit about this and this is my input.

One thing I do not like about the appoach presented before is that it
forces the writer of the dispatch function to consider what happens
both when the function receives an instance of a Condition and when it
receives an arbitrary Throwable. The way you get the data out of a
Condition might not work for a Throwable. Ideally, I think the
dispatch function should be passed the map held by the condition,
since that is what the user "raises" and the only data the user can
vary.

To me it's becoming clear that host integration and arbitrary dispatch
are hard to combine and doing so only complicates the matter. I have
an idea (and a working implementation) of another approach which
involves separating these to scenarios:

http://github.com/raek/map-exception

What follows in this mail is a description of that approach. In a way,
it is (-> Condition meikel-brandmeyer rasmus-svensson), that is, what
you get if you filter Meikel's idea through my mind one iteration.


I think there are two very good ideas around: integration of an error
system with the one of the host, and use a dispatch function to select
the appropriate error action. However, I have started to doubt that a
solution must incorporate both ideas at the same time. An interesting
similar case is the case of multimethods and protocols. Multimethods
are the more general solution that keeps involved conceps orthogonal:
arbitrary dispatch function (not only 'class'), 'isa?' hierarchy not
necessary the host type hierarchy. Protocols were intoduced to solve
the same problem, but by also providing better host integration, and
as a side-effect they have to be less general.

I'm starting to see the same scenario for error handling. We want to
define new error "kinds" without having to resort to gen-class and we
want to keep our options open regarding what to dispatch on. I propose
that we separate the new try into two forms: one that allows us to
write catch cases for both host errors and Clojure errors in the same
way -- I'm going to call it try+ -- and one that can do arbitrary
dispatch, but only on Clojure errors -- I'll call that one
try-multi. I will also present ideas for how the accompanying throw+
looks like.

To give these ideas a try, I made an implementation with parts
adapted/stolen from Meikel's code. I wrote a excpetion class in Java,
because I have heard that AOT in libraries should be avoided, and
because it was such a small class anyway.

These are some thoughts that influenced the design of these map
exceptions or "throwable maps":

* You should be able to use maps in place of exception objects and
namespaced keywords in place of class names.

* From the Clojure side, you interact with a map; the host side
interacts with a PersistentMapException (see below). They should
represent the same data.

* You should be able to deal with host exceptions the same way as
before.

* Host integration and general dispatch should be split into two
separate things. Then, there is no need to define what the dispatch
value for a host exception should be and mean. The semantics gets
simpler this way, I believe.

The new forms use the new exception type PersistentMapException (a
subclass of RuntimeException, suggestions for a better name are
welcomed), which holds a single instance of an IPeristentMap. The map
can be retrieved with the getMap method, and is assoc'ed with the
stacktrace of the exception at the :stack-trace key. The
implementations of getMessage, and getCause are overridden to return
the value associated with the :message and :cause keys of the map. As
usual for a Throwable, initCause can be called to replace the map with
it with :cause assoced to the given argument, but only if the :cause
key was not present before. The class has a single constructor that
takes the map to hold and sets the stacktrace to the value of the
:stack-trace key, if present. toString returns
"PersistentMapException: <type>: <message>", where <type> is the value
for the :type key.

This new exception type is provided to support the illustion of
"throwable maps". Important keys of these maps (pretty much the same
as for Condition) are:

:type - namespaced keyword, default dispatch value
:message - human-readable description
:cause - another Throwable that caused this error
(maps here should be wrapped in map exceptions)
:stack-trace - directly from getStackTrace

My idea is that try+ will be used in same way as try for host
exceptions. My implementation of try+ leaves all non-map clauses
untouched. A try+ without any map clauses simply becomes a plain
try.

Catch clauses for map exceptions are written with a similar syntax to
the host variants, but with the value associated with the :type key
substituted for the exception class. The variable (or destructuring
form) gets bound to the map held by the map exception. A catch clause
is a host catch clause when the type is either a class object or a
symbol that resolves to a class. Otherwise the clause is a map catch
clause.

The PersistentException that transported the map is not exposed. All
information it contained -- including the stack trace -- is included
in the map. To rethrow the exception, use throw+. It will create a new
instance of PersistentException, but it will be initialized with the
old one's stacktrace and continue where it left off.

An example:

(try+
(throwing-code)
(catch ::foo-error m
(str "got a foo error: " (:message m)))
(catch ::bar-error m
(str "got a bar error: " (:message m)))
(catch RuntimeException e
(str "got a runtime exception: " (.getMessage e)))
(catch Exception e
(str "got an exception: " (.getMessage e)))
(finally
(do-something)))

It expands to this (resovled namespaces removed for brevity):

(try
(throwing-code)
(catch PersistentMapException e__1530__auto__
(let [G__1754 (.getMap e__1530__auto__)]
(condp #(isa? %2 %1) (:type G__1754)
::foo-error (let [m G__1754]
(str "got a foo error: " (:message m)))
::bar-error (let [m G__1754]
(str "got a bar error: " (:message m)))
(throw e__1530__auto__))))
(catch RuntimeException e
(str "got a runtime exception: " (.getMessage e)))
(catch Exception e
(str "got an exception: " (.getMessage e)))
(finally
(do-something)))

All the map catch clauses expand into a single host catch clause for
the PeristentMapException type. The try+ form only allows the map
clauses to be grouped together and precede any host clauses (i.e. no
interleaving of map and host clauses). Since PersistentException is
final, it will never shadow any other exception type, and it's safe to
put its catch clause first. If no map catch clause matches, the
exception is rethrown.

The throw+ form can take either a Throwable (just like throw) or an
instance of IPersistentMap. If a map is given, an instance of
PersistentMapException will be constucted and thrown. The helper
function wrap-map-in-exception -- which (along with throw) is what
throw+ expands into -- was implemented using definline, which has the
neat propery of not leaving additional entries in the stack trace.

(defn throwing-code []
(throw+ {:type ::foo-error, :message "Invalid Foo"}))

Expands into:

(defn throwing-code []
(throw (wrap-map-in-exception
{:type ::foo-error, :message "Invalid Foo"})))

And then:

(defn throwing-code []
(throw
(let [x__1619__auto__ {:type ::foo-error, :message "Invalid Foo"}]
(condp instance? x__1619__auto__
Throwable x__1619__auto__
IPersistentMap (PersistentMapException. x__1619__auto__)
(throw (IllegalArgumentException.
"Argument must be a Throwable or an IPersistentMap"))))))

In general, throw+ cannot know whether its argument is a map. An
optimization can easily be done for the case when the argument is a
literal map.

try-multi, the general dispatch variant of try+, only operates on map
exceptions. Host exceptions do not participate and pass right
through. The user can of course add a wrapping try+ form to catch host
exception. The first argument is the dispatch function. What were
types in try+ (Classes or :type values) are dispatch values in
try-multi. The value passed to the dispatch function is simply the map
held by the map exception.

(try-multi (juxt :a :b :c)
(other-throwing-code)
(catch [1 2 3] {:keys [message]}
(str "one-two-three: " message))
(catch [4 5 6] {:keys [message]}
(str "four-five-six: " message))
(finally
(do-something)))

Expands into:

(try
(other-throwing-code)
(catch PersistentMapException e__1534__auto__
(let [G__1620 (.getMap e__1534__auto__)]
(condp #(isa? %2 %1) ((juxt :a :b :c) G__1620)
[1 2 3] (let [{:keys [message]} G__1620]
(str "one-two-three: " message))
[4 5 6] (let [{:keys [message]} G__1620]
(str "four-five-six: " message))
(throw e__1534__auto__))))
(finally
(do-something)))

I also made a try-multi-hierarchy form. With its extra argument you
can specify which hierarchy isa? should use when finding a matching
catch clause. This could perhaps be useful for testing, as it allows
the test suite to divide the universe of map exceptions into arbitrary
groups and hierarchies.


So, what do you think? :-)

// Rasmus

Stuart Halloway

unread,
Dec 16, 2010, 1:41:31 PM12/16/10
to cloju...@googlegroups.com
Please make this proposal a page under http://dev.clojure.org/display/design/Exception+Handling !

Stu

Laurent PETIT

unread,
Dec 17, 2010, 4:14:57 AM12/17/10
to cloju...@googlegroups.com


2010/12/16 Rasmus Svensson <ra...@lysator.liu.se>

Hello Rasmus, pretty comprehensive and good job!

I have a simple question first : can you (or others) feed me with real use cases for a try-multi ?

I also don't know if this has been debated yet, but with the upcoming (or is it already in master ?) arrival of dynamic binding env. propagation through threads for agents, futures, etc., aren't we very close to being able to have a first-class "conditions" system (a la Common Lisp) ?

If so, I think that the current thoughts should embrace this possibility, and not just try to do "a better clojure version of try/catch" ?

Constantine Vetoshev

unread,
Dec 17, 2010, 9:31:30 AM12/17/10
to cloju...@googlegroups.com
On Fri, Dec 17, 2010 at 4:14 AM, Laurent PETIT <lauren...@gmail.com> wrote:
> I also don't know if this has been debated yet, but with the upcoming (or is
> it already in master ?) arrival of dynamic binding env. propagation through
> threads for agents, futures, etc., aren't we very close to being able to
> have a first-class "conditions" system (a la Common Lisp) ?

That would be phenomenal. A Common Lisp style integrated debugger
seems within reach, too.

Reply all
Reply to author
Forward
0 new messages