2011/3/28 Cédric Beust ♔ <[email protected]>
>
>
> On Mon, Mar 28, 2011 at 1:31 PM, Kevin Wright <[email protected]>wrote:
>>
>>
>> This is the problem causing Java's excessive stack traces, just
>> exemplified by Spring more than most other libraries/frameworks:
>> http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html
>>
>>
>
> I like Steve's articles but could you at least be more specific than
> quoting a ten page blog post?
>
>
>>
>> The "best technique" to solve this arguably isn't checked exceptions.
>> It's true alternate return values coupled with a decent implementation of
>> closures to pull much of the boilerplate out of those stack traces.
>>
>
> We have rebutted this claim of yours many, many times. Your proposal is
> completely missing the non local handling aspect of exceptions. I am baffled
> why you keep thinking that return values (even alternate ones) are even in
> the same league as exceptions when it comes to handling errors.
>
Concrete example time :)
Imagine you have a collection of paths. For each path you want to locate a
file and parse the contents to some internal representation.
If any fails, we want to make a note of that and continue with the rest.
In java, the core logic would normally go something like this:
Widget loadWidget(final File file) throws WidgetParseException {
...
return widget;
}
final List<String> paths = ImmutableList.of(
"/usr/home/...",
"/var/lib/...",
"/mount/server1/...",
...);
final List<Widget> widgetBuilder = new ArrayList<Widget>();
for(String path: Paths) {
final File file = new File(path);
final Widget widget = loadWidget(file)
widgetBuilder.add(widget)
}
final List<Widget> widgets = ImmutableList.copyOf(widgetsBuilder)
There's a couple of checked exceptions not being caught, I'll get back to
those.
In Scala, it's a little cleaner:
def loadWidget(file: File) = {
...
widget
}
val paths = Seq(
"/usr/home/...",
"/var/lib/...",
"/mount/server1/...",
...)
val widgets = paths map { path => loadWidget(new File(path)) }
No concerns about checked exceptions, but we now have the potential for
runtime exceptions to screw up the entire operation if just one file can't
be opened, or has a parse error. The exception *is not part of the static
return type* from the File constructor or from loadWidget, if it was - you
could just put that return value (exception or otherwise) straight into a
collection and deal with it later.
One option is to simply trap/log any errors at the tightest possible scope,
but that means there will be a corresponding widget missing in the output,
and more code will be necessary if a way is needed to convey this fact to
calling code. The exception-handling boilerplate will also tend to
obfuscate the essential logic of the code.
Another option is to have null or an Option[Widget] in the output - but this
isn't too different from the first choice, and it still prevents later
stages of the program from seeing which paths couldn't be loaded, as well as
risking later NullPointerExceptions.
A third option is to try/catch around the entire operation and consider that
if any fails then all should fail. But that's not what the specification
called for.
A fourth option is to have the output be a Map[String, Option[Widget]].
This is much better, as information about failures is now encapsulated
within the results. At least you'd then see which paths had failed, but
still not *why*.
A fifth option is to have a ParseResult type, with subclasses ParseSuccess
(holding a widget) and ParseFailure (holding an exception). But this very
quickly ends up being boilerplatey:
public interface ParseResult {
abstract boolean isSuccess();
}
public final class ParseSuccess extends ParseResult {
public final Widget widget;
public ParseSuccess(final Widget theWidget) { this.widget = theWidget; }
public isSuccess() { return true; }
}
public final class ParseFailure extends ParseResult {
public final Exception reason;
public ParseFailure(final Exception theReason) { this.reason =
theReason; }
public isSuccess() { return false; }
}
public Widget loadWidget(final File file) throws WidgetParseException {
...
return widget;
}
final List<String> paths = ImmutableList.of(
"/usr/home/...",
"/var/lib/...",
"/mount/server1/...",
...);
final Map<String, ParseResult> resultsBuilder = new HashMap<String,
ParseResult>();
for(String path: Paths) {
try {
final File file = new File(path);
final Widget widget = loadWidget(file)
final result result = new ParseSuccess(widget)
resultsBuilder.put(path, result)
} catch(Exception ex) {
final result result = new ParseFailure(ex)
resultsBuilder.put(path, result)
}
}
final Map<ParseResult> results = ImmutableMap.copyOf(resultsBuilder)
It solves the problem, but you now also have all the isSuccess checks and
casting to be done whenever you read back that results map... It's also not
very generic, you have to repeat the structure every time you want to reuse
this pattern elsewhere. Another issue is that it's now completely obscured
the entire purpose of the logic, hardly fair on future maintainers!
So checked exceptions *have* achieved their stated goal, of forcing
developers to trap exceptions at the right level. But by being outside of
the normal type system they also force some truly hideous designs and
workarounds. The above code may meet the specified goals, but in doing so
it's left a maintainence nightmare that's likely going to become a magnet
for future bugs, I can foresee copy/paste errors in that particular
codebase.
What then happens if Either[Exception, Widget] is used instead of checked
exceptions? First, we'd have something like this defined just once at the
project level:
def exceptionToLeft[T](block: => T): Either[Exception, T] =
try {
Right(block)
} catch {
case ex => Left(ex)
}
Then:
def loadWidget(file: File): Widget = ...
val paths = Seq(
"/usr/home/...",
"/var/lib/...",
"/mount/server1/...",
...)
val widgets: Seq[Either[Exception, Widget]] = paths map { path =>
exceptionToLeft { loadWidget(new File(path)) }
}
It's now completely obvious exactly what's going on, there's very little
boilerplate, the cost of using this pattern elsewhere is nil, and it's now
trivial to pull out interesting stuff with closures and pattern matching:
val loadedWidgets = widgets.values collect { case Right(x) => x } //yields
an iterable collection of widgets
val failedPaths = widgets collect { case (path, Left(x)) => (path, x) }
//yields a map of paths to exceptions
There are also various ways of composing Eithers and Options that lead to
some even nicer designs, but that's for another day...
It's a lesson we learned in the early 90's.
>
> --
> Cédric
>
>
>
--
Kevin Wright
gtalk / msn : [email protected]
<[email protected]>mail: [email protected]
vibe / skype: kev.lee.wright
quora: http://www.quora.com/Kevin-Wright
twitter: @thecoda
"My point today is that, if we wish to count lines of code, we should not
regard them as "lines produced" but as "lines spent": the current
conventional wisdom is so foolish as to book that count on the wrong side of
the ledger" ~ Dijkstra
--
You received this message because you are subscribed to the Google Groups "The
Java Posse" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to
[email protected].
For more options, visit this group at
http://groups.google.com/group/javaposse?hl=en.