Hacker News new | ask | show | jobs
by mindslight 4669 days ago
Vague value judgments ("only for exceptional conditions") are the inevitable result of a failure to reason.

The semantic function of exceptions is just a way for summing additional values onto the return type of a function because it has results that are not contained within the primary type. In this way, they are a more general and better typed version of NULL (which has it's places - contrary to the modern dogma, these sort of features are needed due to inherent complexity). The standard ways of attempting to avoid this are to either use sentinel values that exist in your standard return type like fd == -1 for an error (thereby making your program less typed), or to create a top-level sum type for every aggregated function return type (cluttering your program with nominal types). Multiple values make the most sense, but those are ad-hoc product types, so you're eschewing the type system in favor of informal invariants.

The syntactic function of exceptions is to avoid constantly repeating (check for error, return error), which often leads to the poor practices of ignoring errors or calling a global exit(). One goal of programming languages is to automate, so it makes sense to capture this oft-repeated pattern. But problems arise when people end up forgetting that every function can have a possible return immediately following it.

It seems some syntactic middleground is needed to signal the complete return type of a function definition, and the possibility that a given function call may quick-return. Honestly (and I hate to say it), but Java probably started down the right track with checked exceptions, but being a B&D language it ended up being waaaay too verbose. And lacking a way to aggregate types along anything but the baked-in hierarchy, people fell into using generic and uninformative 'throws Exception'. And open types make it so there's little point trying to enumerate exhaustive causes. But that doesn't mean that one can't start with the idea of non-silent but syntactically lightweight exceptions and come up with something that avoids a lot of the pitfalls.

6 comments

lol... I've always felt that way about Java
Overall, very good comment. My one point of disagreement is where you dismiss sum types because they clutter your program. Since sum types are the correct answer to this problem in theory, it seems to me that we shouldn't move on from that answer due to problems with implementing it in practice. Further, I think that the problem has already been solved pretty well in languages like Haskell that support type classes (like Monad) which make it so that you no longer have to worry about the sum types except when explicitly working with them (e.g. pattern matching on Left and Right). There's still room for improvement on working with sum types in Haskell (e.g. nested sum types can be annoying), but it's the best solution I've seen so far. Joel even makes note of Haskell's solution being good in the article, but only briefly.
> My one point of disagreement is where you dismiss sum types because they clutter your program ...

> There's still room for improvement on working with sum types in Haskell (e.g. nested sum types can be annoying)

What I dismissed is requiring sum types to be nominal. For instance, imagine that Haskell used the Scheme/Java representation of objects (ie everything is basically a member of one sum type, discriminated on a machine word in the header). We could then do things like:

    type Foo  = Bar | Baz
    type Foo2 = Bar | Baz
Where Bar and Baz could be any type. Now both Foo and Foo2 are just different names for exact same thing, and in fact the names Foo/Foo2 are irrelevant when pattern matching the result of a function that's been declared to return (Bar | Baz). This philosophy is a bit different from Haskell in that it assumes that "everything is an object" (rather than the zero-overhead structs of Haskell), and it implies that every sum type defined this way can only contain one branch for each included type (without names, there's no way to differentiate them), but a merging of the semantics could definitely be hammered out.

I think Rust (being not quite formed yet) could benefit from taking a stab at this, having every sum discriminator be globally unique, and every non-sum type having an associated global tag that only gets prepended when it is promoted to being an anonymous branch of a sum. The immediate use I envision is being able to create ad-hoc type hierarchies that are descriptive rather than prescriptive.

I'm really fond of the idea of Nullable types (where you have to unwrap them by checking if they're null or errors to get the value). I don't think any non-functional language other than Rust has attempted to work this into the way they work yet, but it strikes me as a powerful mechanism that could also allow for deferring error handling to the site most capable of dealing with it.
Scala has the Try [0] and Either [1] types that can be used to achieve this. However, I believe Scala also allows any type to be NULL. In Rust there is no NULL so you don't have to worry if a value is ever NULL unless it is explicitly wrapped in an option instance.

[0] http://www.scala-lang.org/api/current/index.html#scala.util.... [1] http://www.scala-lang.org/api/current/index.html#scala.util....

If you use Guava, you even get Optional<T> for Java. It works pretty well, but is obviously something you have to work into your API and can't easily use after the fact.
Completely agree with the gist of what you are saying, but exceptions are MORE than just a sum/product/extended value type: They are a non-local return. You're obviously aware of that, from the Java comment, but seem to consider that fact an implementation issue of Java, whereas most people consider this a defining attribute of exceptions.
They're restricted to only escaping currently active stack frames, not jumping anywhere like full continuations. They're basically equivalent to having a default cascade of

    if (ret == -1) return -1;
after every function invocation, which is why I was calling that aspect a syntactic feature.

It's this implicit return after every function that trips people up (when prematurely exiting from stateful computation). So what I was saying that making the call of an exception-throwing function (slightly) more verbose would alleviate that and pay for the complexity where it was used.

> Multiple values make the most sense, but those are ad-hoc product types, so you're eschewing the type system in favor of informal invariants.

Maybe the right approach would be to enrich the type system so that it can express those invariants.

You forgot to mention one of the main criticism to use exceptions for errors: the processor penalty.

Related discussions:

- http://stackoverflow.com/questions/8805238/run-time-penalty-...

- http://stackoverflow.com/questions/299068/how-slow-are-java-...

You forgot to mention one of the main criticism to use exceptions for errors: the processor penalty.

As with much of the exceptions debate, it’s important not to over-generalise here.

For example, if you’re writing in a compiled language like C++ and your compiler uses a table-driven implementation for the exception mechanism (as most modern ones did, the last time I checked) then there isn’t necessarily any direct runtime overhead at all when no exception is thrown. In fact, the non-exceptional code path can even run a little faster than equivalent code with manual error handling via return codes, if conditional logic for propagating error codes can be omitted at all the intermediate levels between the one(s) where exceptions are thrown and the one(s) where they are caught.

On the other hand, the possibility of an exception being thrown might interfere with some optimisations. Also, the jump tables can be huge: I once saw the compiled output for a moderately large code base drop in size by 1/3 just from compiling it with exceptions disabled.

In short, there are a lot of factors at play, but anyone who parrots the line that using exceptions always slows things down has never spent much time looking at what actually happens with real compilers. And of course, this is only in one type of compiled language, which doesn’t necessarily imply anything about the performance characteristics of other languages (which vary widely).

Agreed that it's a poor overgeneralization, and often trotted out when it's absolutely incorrect, but it's worth noting that on Windows the existence of Structured Exception Handling in the OS prevented this kind of optimization for a long time. It may yet, in fact, but I've been out of that world for a long time.

This probably extended to things like the xbox as well.

Fortunately that issue has waned with the decline in the x86.