The Project Lambda mailing list has been considering
exception transparency recently.
My fear with the proposal in this area is that the current proposal goes beyond what Java's
complexity budget will allow. So, I proposed an alternative.
Exception transparency
Exception transparency is all about checked exceptions and how to handle them around a closure/lambda.
Firstly, its important to note that closures are a common feature in other programming languages.
As such, it would be a standard approach to look elsewhere to see how this is handled.
However, checked exceptions are a uniquely Java feature, so this approach doesn't help.
Within Neal Gafter's BGGA and CFJ proposals, and referenced by the original
FCM proposal is the concept and solution for exception transparency.
First lets look at the problem:
Consider a method that takes a closure and a list, and processes each item in the list using the closure.
For out example, we have a conversion library method (often called map) that transforms an input
list to an output list:
// library method
public static <I, O> List<O> convert(List<I> list, #O(I) block) {
List<O> out = new ArrayList<O>();
for (I in : list) {
O converted = block.(in);
out.add(converted);
}
return out;
}
// user code
List<File> files = ...
#String(File) block = #(File file) {
return file.getCanonicalPath();
};
List<String> paths = convert(list, block);
However, this code won't work as expected unless we specially handle it in closures.
This is because the method getCanonicalPath
can throw an IOException
.
The problem of exception transparency is how to transparently pass the exception, thrown by the user supplied closure,
back to the surrounding user code.
In other words, we don't want the library method to absorb the IOException
,
or wrap it in a RuntimeException
.
Project Lambda approach
The approach of Project Lambda is modelled on Neal Gafter's work.
This approach adds addition type information to the closure to specify what checked exceptions can be thrown:
// library method
public static <I, O, throws E> List<O> convert(List<I> list, #O(I)(throws E) block) throws E {
List<O> out = new ArrayList<O>();
for (I in : list) {
O converted = block.(in);
out.add(converted);
}
return out;
}
// user code
List<File> files = ...
#String(File)(throws IOException) block = #(File file) {
return file.getCanonicalPath();
};
List<String> paths = convert(list, block);
Notice how more generic type information was added - throws E
.
In the library method, this is specified at least three time - once in the generic declaration, once in the
function type of the block and once on the method itself.
In short, throws E
says "throws zero-to-many exceptions where checked exceptions must follow standard rules".
However, the user code also changed. We had to add the (throws IOException)
clause to the function type.
This actually locks in the exception that will be thrown, and allows checked exceptions to continue to work.
This creates the mouthful #String(File)(throws IOException)
.
It has recently been noted that syntax doesn't matter yet in Project Lambda.
However, here is a case where there is effectively a minimum syntax pain.
No matter how you rearrange the elements, and what symbols you use, the IOException
element needs
to be present.
On the Project Lambda mailing list I have argued that the syntax pain here is inevitable and unavoidable with
this approach to exception transparency.
And I've gone further to argue that this syntax goes beyond what Java can handle.
(Imagine some of these declarations with more than one block passed to the library method, or with wildcards!!!)
Lone throws approach
As a result of the difficulties above, I have proposed an alternative - lone-throws.
The lone-throws approach has three elements:
- Any method may have a
throws
keyword without specifying the types that are thrown ("lone-throws").
This indicates that any exception, checked or unchecked may be thrown.
Once thrown in this manner, any checked exception flows up the stack in an unchecked manner.
- Any catch clause may have a
throws
keyword after the catch
.
This indicates that any exception may be caught, even if the exception isn't known to be thrown by the try
block.
- All closures are implicitly declared with lone throws.
Thus, all closures can throw checked and unchecked exceptions without declaring the checked ones.
Here is the same example from above:
// library method
public static <I, O> List<O> convert(List<I> list, #O(I) block) {
List<O> out = new ArrayList<O>();
for (I in : list) {
O converted = block.(in);
out.add(converted);
}
return out;
}
// user code
List<File> files = ...
#String(File) block = #(File file) {
return file.getCanonicalPath();
};
List<String> paths = convert(list, block);
If you compare this example to the very first one, it can be seen that it is identical.
Personally, I'd describe that as true exception transparency (as opposed to the multiple declarations
of generics required in the Project Lambda approach.)
It works, because the closure block automatically declares the lone-throws.
This allows all exceptions, checked or unchecked to escape.
These flow freely through the library method and back to the user code.
(Checked exceptions only exist in the compiler, so this has no impact on the JVM)
The user may choose to catch the IOException, however they won't be forced to.
In this sense, the IOException has become equivalent to a runtime exception because it was wrapped in a closure.
The code to catch it is as follows:
try {
paths = convert(list, block); // might throw IOException via lone-throws
} catch throws (IOException ex) {
// handle as normal - if you throw it, it is checked again
}
The simplicity of the approach in syntax terms should be clear - it just works.
However, the downside is the impact on checked exceptions.
Checked exceptions have both supporters and detractors in the Java community.
However, all must accept that given projects like Spring avoiding checked exceptions, their role has been reduced.
It is also widely known that other newer programming languages are not adopting the concept of checked exceptions.
In essence, this proposal provides a means for the new reality where checked exceptions are less important to be accepted.
Any developer may use the lone-throws concept to convert checked exceptions to unchecked ones.
They may also use the catch-throws concept to catch the exceptions that would otherwise be uncatchable.
This may seem radical, however with the growing integration of non-Java JVM languages, the problem of being
unable to catch checked exceptions is fast approaching.
(Many of those languages throw Java checked exceptions in an unchecked manner.)
As such, the catch-throws clause is a useful language change on its own.
Finally, I spent a couple of hours tonight implementing the lone-throws and catch-throws parts.
It took less than 2 hours - this is an easy change to specify and implement.
Summary
Overall, this is a tale of two approaches to a common problem - passing checked exceptions transparently from
inside to outside a closure.
The Project Lambda approach preserves full type-information and safeguards checked exceptions at the cost
of horribly verbose and complex syntax.
The lone-throws approach side-steps the problem by converting checked exceptions to unchecked, with
less type-information as a result, but far simpler syntax.
(The mailing list has discussed other possible alternatives, however these two are the best developed options.)
Can Java really stand the excess syntax of the Project Lambda approach?
Or is the lone-throws approach too radical around checked exceptions?
Which is the lesser evil?
Feedback welcome!