The problems created by not having exceptions are tougher than the problems created by having exceptions.
The problems created by not having exceptions are tougher than the problems created by having exceptions.
Did you really find the example imperative error handling code more elegant than the railway-style function composition?
1) Good luck annotating all potential cases of divide by zero or IO error, and all callers of those.
2) Everyone must pay the runtime cost of wrapping and unwrapping on every operation, even if nothing fails.
3) The type of a function that can fail in ways A and B isn't the same as the type of a function that can fail in ways B and A.
4) Good luck getting a stack trace.
I think unchecked exceptions are better for most applications, where errors are rare and have many different types.
That's a great point. At the end of the day, Either and (checked) exceptions are equivalent from a semantic point of view, but having exceptions being an explicit part of the language means much better support for tooling and debugging.
Exceptions are not too bad, but they are very often misused.
This! The worst is when people start using them to control the flow of business logic. I hate that.
I'm a data scientist, not the software engineer, so forgive me if this is something you all learned in CS 101.
Also, constructing an exception object, with the full stacktrace etc., is a pretty heavy operation and can cause big performance issues. But it's mostly the first thing.
* Which is an important distinction. Flow control exception are recognisable because you intentionally throw an exception and expect it to be caught by a particular catcher to process it in a particular way - much like a GOTO, and with all its fragility. In the proper usage of exceptions, when you throw an exception you don't care which line of code catches it and when you catch an exception you don't care which line of code threw it.
Validation cases like the examples on the article are not exceptional and the failure cases must be handled properly. In this case it makes sense to use an Either -like solution where the compiler will warn if there are unhandled cases.
Both styles of error handling are useful in some cases and both have their issues.
As for 2) the exceptions are not zero-costs even with modern implementations and profile-driven optimizing compiler makes the runtime cost of wrapping/unwrapping low.
For point 1, if you want to use the railway approach instead of exceptions, you must annotate all functions in the chain.
For point 2, I was talking about the runtime cost.
For point 3, Result<Result<Foo,A>,B> isn't the same type as Result<Result<Foo,B>,A>. People in the Haskell world acknowledge that it's a big problem ("monads don't compose"), hence all the research on monad transformers and extensible effects etc.
For point 4, if your function is the lowest in the call stack and decides to report an error using the railway approach, you don't get a stack trace.
Foo -> Result<Foo1,Error>
Foo1 -> Result<Foo2,Error>
Foo2 -> Result<Foo3,Error>
And use bind to compose them easily.I think the confusing part is the word "exception" as it mixes concepts.
It's not goto's that were so painful, it was unstructured gotos. Calling exceptions gotos is not a reasonable criticism. They have some very well-defined structure, and additional benefits like separating your happy-path code from your error-handling code, making both simpler.
- Static binding of location to jump to. This is what goto, callcc, while, for etc. do.
- Dynamic binding of location to jump to. This is what exceptions do.
You can show that they are really different, in the sense that (in the absence of powerful language features), they cannot simulate each other, see [1]. Once you realise that exceptions and gotos are quite different thinking about error handling becomes more clearer.
[1] J. G. Riecke, H. Thielecke, Typed Exceptions and Continuations Cannot Macro-Express Each Other.
When a language support contructions such that normal control flow event are either "success + result", or "failure + error", it's much easier to follow, for me, and experience has taught me that it is far less error prone. Exceptions have a use for things that are unexpected, but happened anyway, such as OutOfMemoryException.
So a function returning T can actually return T or one of the set of Exception types.
The return type of a function is always handled by its direct caller and only by its direct caller.
Exceptions are fundamentally different because they aren't necessarily handled by the caller, but by the nearest appropriate exception handler, wherever it is, regardless of scope.
That is both their strength when they are used appropriately, and their weakness when they are not.
That implicit parallel 'track' is useful in doing the Foo => Result<Foo, Error> for you in the form of Result<Foo, Exception> but bad in not warning of unmatched values and hiding the fact. Like many a tool it can be abused by those who know just enough to use it but not enough to know why.
This is why Java originally forced you to make thrown exceptions explicit. This railway approach is one good way of dealing with error conditions explicitly.
For example, the Maybe monad is the simplest way to handle a function which might not return a usable value: A value wrapped in a Maybe monad has a type of either (Just a) (with a being any type at all) or Nothing. Here's a trivial example, straight from the ghci REPL:
> let mdiv x y = if y == 0 then Nothing else (Just (x / y))
> let foo x = (Just (x + 5))
> let bar y = (Just (y / 2))
> mdiv 5 6 >>= foo >== bar
Just 2.9166666666666665
> mdiv 5 0 >>= foo >>= bar
Nothing
See? Complete insensitivity to errors until the last possible moment. Much less noisy than the C equivalent would be, although in this specific example I suppose the Inf semantics of the IEEE floating point standard would be equivalent.