4 ms·
I've been using Scala quite heavily recently, having mostly used Haskell for years, with distant memories of Java. Scala allows Java methods to be called, but d
by chriswarbo 6y ago
I've been using Scala quite heavily recently, having mostly used Haskell for years, with distant memories of Java. Scala allows Java methods to be called, but doesn't bother with checked exceptions, which has bitten me quite a few times.
My preferred style of error-handling is Option/Either, since I can implement the 'happy path' in small, pure pieces; plug them together with 'flatMap', etc.; then do error handling at the top with a 'fold' or 'match'.
Exceptions break this approach; but it's easy to wrap problematic calls in 'Try' (where 'Try[T]' is equivalent to 'Either[Throwable, T]').
The problem is that Scala doesn't tell me when this is needed; it has to be gleaned from the documentation, reading the library source (if available), etc.
I get that a RuntimeException could happen at any point; but to me the benefit of checked exceptions isn't to say "here's what you need to recover from", it's to say "these are very real possibilities you need to be aware of". In other words checked exceptions have the spirit of 'Either[Err, T]', but lack the polymorphism needed to make useful, generic plumbing. The article actually points this out, complaining that checked exceptions have to be handled/declared through 'all intervening code'; the same can actually be said of 'Option', or 'Either', or 'Try', etc., but the difference is that their 'intervening code' is usually calculated by the higher-order functions provided by Functor, Applicative, Monad, Traverse, etc.
It's similar to many developer's first experience of Option/Maybe: manually unwrapping them, processing the contents, wrapping up the result, then doing the same for the next step, and so on. It takes a while to grok that we can just map/flatMap each of our steps on to the last (or use 'for/yield', do-notation, etc. if available). It would be nice to have a similar degree of polymorphism for checked exceptions. Until then, I'd still rather have them checked (so I can convert them to a 'Try'), rather than getting no assistance from the compiler at all!
- kevmo314 6y agoThe compiler can still help without needing to resort to checked exceptions. For example, `Either[T, Either[FileNotFoundCheckedException, RuntimeException]]` is basically isomorphic to a function that returns T or throws and has the same monad support, but can still force the developer to handle the checked exceptions. I'm not sure if it really addresses the underlying concern that the article presents though, which seems more like checked exceptions seem to be used in a way where the developer has no recourse anyways, so surfacing it through a monad or checked exception doesn't matter.
- chriswarbo 6y ago> For example, `Either[T, Either[FileNotFoundCheckedException, RuntimeException]]` is basically isomorphic to a function that returns T or throws and has the same monad support, but can still force the developer to handle the checked exceptions. Yes, and I tend to write my code this way (although I find right-biased 'Either' a bit cleaner). The problem is (a) the mountain of JVM code which uses checked exceptions instead plus (b) the Scala compiler completely ignoring that exception information. Solving (a) is unrealistic, but (b) is an entirely self-imposed decision by the Scala developers. Their type checker could have incorporated checked exceptions in exactly the way you describe, and I wouldn't have any complaints (about exceptions, at least... null is a whole different beast ;) )
- barrkel 6y agoThe right way isn't Either / Option (though it's a lot more viable with top-level type inference); it's unchecked exceptions. Checked exceptions have bimodal usage, from the programmer POV. Either you care about the exception, and you deal with it very close to the throw point, or you don't care about the exception, and it should be handled far far away, across many stack frames. The former isn't a problem. It's the right thing if e.g. you try to open a file and the file is missing, and you have reasonable error handling logic to retry, open a different file, replace the file and try again, whatever. The latter is where the issue lies. If you're handling errors far away, then you're handling lots of different errors there, and you're not distinguishing between them, because there's too many different failure modes. You're most likely just in a loop logging errors, or terminating. You're too far from the cause of the exception to do anything specific with it, the context is lost. So the effort to transport the exception type throughout the call graph is pointless. tl;dr: checked exceptions are fine near the leaf of the call graph, but are increasingly pointless towards the trunk.
- xienze 6y ago> You're too far from the cause of the exception to do anything specific with it, the context is lost. Which is perfectly fine. If you _can_ handle the exception near where it happened, do so. Otherwise bubble it up and log it or whatever. Consider a REST service. If a DB or other error happens, I can retry right then and there if business logic calls for it (probably not), or just kick the can down the road where something appropriate like a 500 response serializer will take care of it. That’s a great pattern in my opinion.
- overraskning 6y agoI think you're making a good point here if you look at it as an application developer. In my experience, checked exceptions are helpful when using a library. Would you recommend this approach for libraries as well or do checked exceptions have more of a role to play there?
- ragnese 6y agoSo you're saying checked exceptions are perfectly fine. I'll write my library code with checked exceptions. You call my library code in one of your methods. The compiler tells you to do something about the possible failure. If you want to handle it there, you handle it with a try{}catch{}. If you don't, you wrap it in a RuntimeException and rethrow it so your top level handler can deal with it. Perfect. Unchecked exceptions make it easy to fuck up the case where you actually might want to handle it close to the call.
- Atomskun 6y agoWhat's the difference to modeling the "happy path" with function calls one after another, with an outermost try block that can catch any exception that might have happened inbetween? These functions wouldn't even need to care about Some[T]; T is enough.
- chriswarbo 6y agoThere are three possibilities: | Polymorphic exceptions | Monomorphic exceptions ---------------------+------------------------+------------------------ Checked exceptions | Unsupported | Java ---------------------+------------------------------------------------- Unchecked exceptions | Scala If exceptions are unchecked then there is no difference between "inner" code and "outermost" code; the compiler cannot tell us that a handler is needed/missing. This is the case in Scala. The advantage is that we can compose code which throws and which doesn't throw (this is essentially dynamic typing for exceptions). If exceptions are checked then there is a difference between "inner" code and "outermost" code: the inner code has 'throws Foo' annotations, the "outermost" code doesn't. The compiler will spot missing handlers (i.e. when our outermost code can throw). There are two ways this could be done: If checked exceptions aren't polymorphic then we need to make multiple versions of higher-order functions, like List::map: one version which doesn't throw, one which can throw one exception, one which can throw two exceptions, etc. (these exceptions can be kept generic, but the number of them must be explicit). For example if we have a lambda which can throw KeyNotFound we can't use it with the standard List::map method, since that only accepts lambdas which don't throw. We could make an alternative method 'public List<B> mapE(FunctionE<A, B, E> f) throws E', but that wouldn't work for lambdas which can throw FileNotFound and PermissionDenied; we could write a 'public List<B> mapEE(FunctionEE<A, B, E1, E2> f) throws E1, E2', but that wouldn't work for three exceptions, and so on. AFAIK this is the current situation in Java. If checked exceptions could be polymorphic, similar to row polymorphism ( https://en.wikipedia.org/wiki/Row_polymorphism https://en.wikipedia.org/wiki/Row_polymorphism ) or algebraic effect systems ( http://lambda-the-ultimate.org/taxonomy/term/35 http://lambda-the-ultimate.org/taxonomy/term/35 ), then we would have the best of both worlds. In this setup the 'E' in an annotation like 'throws E' doesn't stand for a name, but for a set of names. Higher-order functions like 'map' can throw the same set of exceptions as the lambda they're given, and that set could have any size: if the lambda can't throw any exceptions then map's set of exceptions is empty; if it can throw five types of exception then map's set of exceptions contains those five; and so on. This is becomes even clearer for things like function composition: public Function<A, C> compose(Function<B, C> f, Function<A, B> g) If 'f' can throw something from set E1 and 'g' can throw something from set E2, then 'compose(f, g)' can throw something from set E1∪E2. Likewise if something can throw E1 and we have handlers for E2, then the result can throw E1 \ E2. AFAIK the JVM can't do this, nor can those languages which typically target it (Java, Scala, Kotlin, etc.; Idris has algebraic effects and it can run on the JVM, although it's not the standard target)
- mlk 6y agoI absolutely agree, a checked exception is pretty much a Either<Exception, RESULT>, it only has a different (arguably ugly) syntax, I don't get all the hate. A "sufficiently intelligent compiler" could have turned signature with throws declarations in signature returning Either<Exception, T> but the hivemind has spoken and checked exception were simply ignored (see Scala and Kotlin)
- ragnese 6y agoKotlin made the worst mistake here. At least Scala actually gives you ways to do error handling somewhat ergonomically with for comprehensions. Kotlin gives us nothing. Sure, you're "supposed" to return sealed classes and match on them. But it's so tedious and non-obvious that I've literally never seen it done in any Kotlin code I've depended on.
- benjiweber 6y agoThey're so similar you can easily translate between the two e.g. https://github.com/unruly/control/blob/master/src/test/java/examples/ExceptionsInStreamsHandling.java https://github.com/unruly/control/blob/master/src/test/java/...