How can the
EncodingException
part of the code below expose implementation details when void encode()
does not? The first say I can fail to encode, the latter say I can encode. What's the difference?
interface Encoder {
void encode(Object o) throws EncodingException;
}
For me, checked exceptions are vital to enforce proper error handling. But this can (probably) only be achieved if the exceptions fits the problem domain. For instance, throwing an
IOException
in the interface above would be really really bad because it exposes details of the Encoder
, e.g., that is uses the network, or whatever. On the other hand, the only thing EncodingException
exposes is that an Encoder
can fail to encode the provided object. That's not an implementation detail, I think, that's the Encoder
being honest and not hiding it flaws.One of the most important lesson I've learnt from using exceptions is the importance of doing try-catch-wrap-throw. For example, if an implementation of
Encoder
uses a method that throws IOException
then the proper way of handling such exception is to catch it, wrap it in a EncodingException
and throw it to the client of the Encoder
. This cleans up ugly interfaces that throws many exceptions, resulting in clear description (with semantics, yeay!) of what can go wrong when a method is called. Exceptions that fits the problem domain is the key.A couple of times I've let domain exceptions inherit from each other to define that, for example, a
RangeException
is a ConfigurationException
. This seemed like a good idea to me at the time, however, it seldom helped the design (it didn't make it worse either, though). In fact, the only time I find it useful is when you need to distinguish between thrown exceptions in one place but handle them in the same way in a nother place. For example (where LeftException
and RightException
inherits from BaseException
):
interface Something {
void doIt() throws LeftException, RightException;
}
class HandlesExceptionsSeparately {
void method(Something s) {
try {
s.doIt()
} catch(LeftException e) {
// handle it
} catch (RightException e) {
// handle in another way) {
}
}
}
class HandlesExceptionsTogether {
void method(Something s) {
try {
s.doIt()
} catch(BaseException e) {
// handles both
}
}
}
But as I said, this is not very common for me. Although if you are developing a library and wish the user to have the ability to handle the different error-cases separetely, then could be useful.
Well, these are some of my thoughts on checked exceptions. In summary: I think they're good stuff if done well. :)
11 comments:
An alternative to checked exceptions that is more composable is the Either monad.
Replace: int parse(String s) throws Ex
With: Either[int, Reason] parse(String s)
There is also the Maybe (aka Option) monad for when there is no failure reason.
You could also use Option[int, Reason]
Option, everywhere I've seen it, has only one type parameter. So Option[int, Reason] would be unusual. The usual name for that is Either.
@Ricky: What do you mean by "more composable"? Exceptions seem pretty composable to me.
didroe,
Consider writing a test that says: for every odd number y between 1 and 100, method x(y) throws an IOException.
for (int a = 1;a < 100;a += 2)
. . try
. . {
. . . . x(a);
. . . . fail();
. . }
. . catch (IOException e)
. . {
. . }
Now consider what happens if x returns Either[Integer, Failure].
for (int a=1; a < 100; a += 2)
. . assertTrue(x(a).isRight);
And with a more convenient syntax (Scala):
1 to 100 by 2 forall (a => x(a).isRight)
The try..catch syntax is statement orientated, i.e., you can't *compose* expressions that do any processing of exceptions.
@Ricky: That test-case would be so much easier to write if Java had closures. In Ruby we have 'assert_raise(SomeException) { piece_of_code }' that tests that a piece of code raises a specified exception.
Actually, closures are not needed per se... it just gives much better syntax.
As I don't know much about Funcitonal Java and haven't tried using Either there are probably cases where Eiter is much better than the closure-approach.
I don't, however, like that error cases are indicated by a return value that you explicitly have to check. But then again, I've never used Either so I'm probably missing somthing in all my ignorance. :)
togge,
You're quite correct, if Java had closures the test case would be easier to write.
Range.from(1, 100).by(2).forall( { int a => raises(SomeException.class){ x(a); } } );
seems to be the best I can get with the BGGA proposal for Java closures, if I invent the Range type with from(int, int), by(int), forall({int => boolean}).
Given your Ruby implementation of this test case, how big a difference is it if you want to adjust the test case to show which elements cause failures? I think it's difficult, because assert_raise itself throws an exception, but I might be wrong.
Either is different to the traditional C-style error code that doesn't get checked, because code that uses Either would typically not otherwise have a void return type. And assuming you read the return value you cannot forget to check it, as your code just won't compile if you do that.
int foo() throws IOException; becomes:
Either[Integer, Failure] foo();, and there's no way to get at the Integer without also 'caring' about the Failure.
Either is more of a concept than necessarily what's in Functional Java, but it's likely that FJ's version is quite sound. I've used FJ once only - most of the features I would use are fun to roll myself. And for work I might need different names, e.g., F becomes Conversion.
It pretty easy to add a message to the assertion:
(1..10).step(2) { |n|
__assert_raise(Exception, "#{n} throws exception") {
____piece_of_code(n)
__}
}
Thank's for the explanation of Either. I understand now that it works differently in functional languages, where the only "side-effect" of a function is its return value. Ignoring a return value is impossible since that the only reason the function is called! :)
I think I need to be clearer:
"Given your Ruby implementation of this test case, how big a difference is it if you want to adjust the test case to show *ALL THE ELEMENTS THAT* cause failures?"
Your version only shows the first, because the assert dumps out of the method at the first problem. In and of itself that's not a bad thing, but because you need to make large (well, large is an overstatement for such small amounts of code) changes for small changes in behaviour, things aren't ideal.
I'm glad you found the explanation of Either interesting.
Ah, yes, that would make the test-case more convoluted and hard to understand. In fact, I'm not even going to try to implement it... I would probably mess it up. :)
I understand what you ment with "more composable" now...
Nice article. I didn't realize that it's possible to expose implementation details this way.
Post a Comment