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
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.
>
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/
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
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
Stu
That would be phenomenal. A Common Lisp style integrated debugger
seems within reach, too.