<div dir="auto"><div>David,</div><div dir="auto">The discussion about checked vs. unchecked exceptions is quite common. The blog Brain linked is very vocal about their opinion, but it is far from the only opinion.</div><div dir="auto"><br></div><div dir="auto">I know people who prefer by a great margin the ease of use of unchecked exception, and how you can handle them without the restrain of the language type system (see below for more info). To be clear, I completely agree with this view and I am a strong believer that checked exceptions are better, and that their potential is not fully realized in Java, see <a href="https://koka-lang.github.io/koka/doc/index.html">https://koka-lang.github.io/koka/doc/index.html</a> for a true full implementation of effect system (generalisation of checked exceptions).</div><div dir="auto"><br></div><div dir="auto"><br></div><div dir="auto">> I find that most functions attempt to handle "the good values" of the ADT.</div><div dir="auto"><br></div><div dir="auto">Just like how people like uniformity in the type system, people like uniformity in their code.</div><div dir="auto">We try to put all failures in few as possible boxes.</div><div dir="auto">When using Java's exceptions it is "exception", "runtimeException" and "error", all of which are under "throwable", in ADT it is usually "optional" and "either" where either has generic parameters (which replace the inheritance we use in exceptions), (in this case DivResult will be an instance of either).</div><div dir="auto"><br></div><div dir="auto">When you have uniformity, you can easily delay handling the failure case to the appropriate time.</div><div dir="auto"><br></div><div dir="auto">In addition, "living in the monad space" usually simplify logic, instead of going back and force from values to monads:</div><div dir="auto"><br></div><div dir="auto">Say I work on float type/double type, and I write a method that tries to solve an equation:</div><div dir="auto"><br></div><div dir="auto"> Either<...> solve(String expr)</div><div dir="auto"><br></div><div dir="auto">(The generic type of the result omitted on purpose)</div><div dir="auto">The flow of this method is "validate input" -> "simplify" -> "validate again" (maybe after simplifying there is division by 0, or root-if-negativr) -> "try to solve with a strategy" -> "collect results of strategies".</div><div dir="auto"><br></div><div dir="auto">Now, failures of "strategy" need to be handled in "solve", but failures in "validate" or "simplify" need to pass to the return value.</div><div dir="auto"><br></div><div dir="auto">Now you can implement it as:</div><div dir="auto"><br></div><div dir="auto">var validation = validate(expr)</div><div dir="auto">if (validation is Either.right(_)) return validation</div><div dir="auto">var simplifyExpr = simplify(expr)</div><div dir="auto">if ...</div><div dir="auto"><br></div><div dir="auto">Or:</div><div dir="auto"><br></div><div dir="auto">return validate(expr)</div><div dir="auto"> .mapLeft(simplify)</div><div dir="auto"> .mapLeft(...)</div><div dir="auto"><br></div><div dir="auto">Or</div><div dir="auto"><br></div><div dir="auto">var validate = validate(expr)</div><div dir="auto">var simplified = simplify(expr)</div><div dir="auto">var ....</div><div dir="auto"><br></div><div dir="auto">While in the last solution, all of the methods looks like:</div><div dir="auto">switch(input)</div><div dir="auto"> Either.right(_) -> input</div><div dir="auto"> Either.left(var val) -> ...</div><div dir="auto"><br></div><div dir="auto">I think that the second and third options are obviously much clearer (especially if you consider "if" to be an anti pattern), the advantages of the last option is that it solves the common problem with pipes like code, of having dependency on older variables (as the absolute majority of programming languages are not affine)</div><div dir="auto"><br></div><div dir="auto">Now, if your language support monads, then just how you can lift a value into Either, you could lift a function U->T and U->Either[T,_] into Either[U,_]->Either[T,_], so when you have a list of methods it is a lot more natural to just work with the same monad all the way (but of course, if you know how to handle *all* the failure options gracefully, you should lift it down before returning, and if the caller need, they will lift your method).</div><div dir="auto"><br></div><div dir="auto">> I am missing out on that chaining map-reduce style flow state that ADT's get<br><br>If you take a look at Koka I linked above, you can see that it is actually possible to have chaining with effect based system. Java just don't have sum and difference types on generic level. Which is a big reason why some people love unchecked exceptions.</div><div dir="auto">If semantically you know about all of the exceptions, you can do stuff like:</div><div dir="auto"><br></div><div dir="auto">Assume func is a method whose possible exceptions are unchecked exceptions, A,B,C:</div><div dir="auto"><br></div><div dir="auto">Stream.generate(...)</div><div dir="auto"> .handle(A.class, (A a) -> handleA(...))</div><div dir="auto"> .handle(B.class, (B b) -> handleB(...))</div><div dir="auto"> .forEach(func)</div><div dir="auto"><br></div><div dir="auto">Then you know that the above chain type is: _->() throws A+B+C-A-B+rA+rB</div><div dir="auto"><br></div><div dir="auto">(Where handleX is of type X->() throws rX)</div><div dir="auto">Java's type system don't have this ability, so you either have all of your lambdas throw Throwable, or use unchecked exceptions.</div><div dir="auto"><br></div><div dir="auto">> comparing apples and oranges</div><div dir="auto"><br></div><div dir="auto">These are 2 possible designs for languages/systems, so it is important to compare them.</div><div dir="auto"><br></div><div dir="auto"><br></div><div dir="auto"><br><div class="gmail_quote" dir="auto"><div dir="ltr" class="gmail_attr">On Wed, 6 Dec 2023, 07:36 David Alayachew, <<a href="mailto:davidalayachew@gmail.com">davidalayachew@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div dir="ltr"><div style="font-family:monospace" class="gmail_default">Hello Brian,<br><br>Thank you for your response!<br><br>> Error handling is hard, no matter how you slice<br>> it. (See<br>> <a href="https://joeduffyblog.com/2016/02/07/the-error-model/" target="_blank" rel="noreferrer">https://joeduffyblog.com/2016/02/07/the-error-model/</a><br>> for a mature analysis of all the options.) <br><br>That was a really nice read! And yes, it was definitely comprehensive, no question.<br><br>I especially appreciated the focus on how Java does Checked Exceptions and Unchecked Exceptions, as well as the strengths and weaknesses of both. I had never really done much trying to make or catch Unchecked Exceptions, but seeing how they "poison the water" was insightful. It was also really interesting to see them talk about how they used assert. Makes me wonder what Java would look like if we had precondition/postcondition tools like that. Or if assert had meaning in the method signature.<br><br>> The benefit of using algebraic data types<br>> (e.g., the Either or Try monads) for error<br>> handling is _uniformity_. Type systems such as<br>> the simply typed lambda calculus (Peirce, Ch9)<br>> say that if we have types T and U, then the<br>> function type `T -> U` is also a type. Such<br>> function types are well behaved, for example<br>> they can be composed:<br>> if f : T -> U and g : U -> V, then g.f : T -> V<br>> <br>> A Java method<br>> <br>> static int length(String s)<br>> <br>> can be viewed as a function String -> int. But<br>> what about a Java method<br>> <br>> static int parseInt(String s) throws NumberFormatException<br>> <br>> ? This is a _partial function_; for some<br>> inputs, parseInt() does not produce an output,<br>> instead it produces an effect (throwing an<br>> exception.) Type systems that describe partial<br>> or effectful computations are significantly<br>> more complicated and less well-behaved. <br>> <br>> Modeling a division result as in the following<br>> Haskell data type:<br>> <br>> data DivResult = Success Double | Failure<br>> <br>> means that arithmetic operations are again total:<br>> <br>> DivResult divide(double a, double b)<br>> <br>> This has many benefits, in that it becomes<br>> impossible to ignore a failure, and the<br>> operation is a total function from double x<br>> double to DivResult, rather than a partial<br>> function from double x double to double. One<br>> can represent the success-or-failure result<br>> with a single, first-class value, which means I<br>> can pass the result to other code and let it<br>> distinguish between success and failure; I<br>> don't have to deal with the failure as a<br>> side-effect in the frame or stack extent in<br>> which it was raised. The common complaints<br>> about "lambdas don't work well with exceptions"<br>> comes from the fact that lambdas want to be<br>> functions, but unless their type (modeled in<br>> Java with functional interfaces) accounts for<br>> the possibility of the exception, we have no<br>> way to tunnel the exception from the lambda<br>> frame to the invoking frame. <br><br>So, this was interesting to read. And they said something similar in the article that you linked.<br><br>On the one hand, that uniformity allows us to cleanly and easily latch/compose things in a happy path sort of way.<br><br>But maybe I am wrong here, but when we are talking about Sealed Types (Algebraic Data Types), I find that most functions attempt to handle "the good values" of the ADT. Obviously, there are things like Stream or Optional that actually do behave the way that this is described as, but how often are we writing something that rich and involved? Usually, we are doing something more akin to DivisionResult. And in those cases, do we really want to hand over DivisionResult as a parameter as opposed to Success? I guess I just don't see much value in passing the "uncertainty" over to the next level unless that uncertainty is the desirable part (again, Stream and Optional).<br><br>My reasoning for this is from an article by Alexis King (Lexi Lambda) -- "Parse, don't validate" (<a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/" target="_blank" rel="noreferrer">https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/</a>). For me, ADT's are at their best when you can extract the good from the bad, then pass along the safer, more strongly typed good, and then handle the safer, more strongly typed bad as desired. The idea of continuing to pass along the "Unknown" type seems to me like passing along the uncertainty when I could have certainty - whether good or bad.<br><br>> It is indeed true that exceptions carry more<br>> information, and that information comes at a<br>> cost -- both a runtime cost (exceptions are<br>> expensive to create and have significant memory<br>> cost) and a user-model cost (exceptions are<br>> constraining to deal with, and often we just<br>> throw up our hands, log them, and move on.) <br>> On the other hand, algebraic data types have<br>> their own costs -- wrapping result<br>> success/failure in a monadic carrier intrudes<br>> on API types and on code that consumes results.<br><br>Ok, cool. So it's a trade off between performance and information.<br><br>I guess that leads to the next question then -- when does it make sense to include that level of information? Knowing that this is the primary difference sends me down some interesting and probably incorrect trains of thought. If the difference is purely a contextual one, then unless I want context, I should default to an ADT. And it is only when I want context that I should opt into Exceptions. How do you feel about that?<br><br>But that gets even more nuanced because then I am missing out on that chaining map-reduce style flow state that ADT's get. Exceptions don't have that. It really feels like I am comparing apples and oranges, and it's not really a matter of which is better, as opposed to if my context happens to want apples vs oranges at that particular moment.</div><div style="font-family:monospace" class="gmail_default"><br></div><div style="font-family:monospace" class="gmail_default">I can see use cases for both. For example, if I wanted to implement retry functionality, it becomes clear almost immediately that Exceptions are a much better vehicle for this then ADT's. Yes, you could easily retry if ServiceException or something. But we don't want to retry infinitely. Thus, you need to know which function and in which context we failed. And what if the second time we reattempt, we get further but get a ServiceException on the next statement? Start from 0 or continue on? What if the user wants the choice? All of this is way easier if you have a stack trace. Whereas for ADT's, the only (easy) way you could do that is if you pass in an instance of your ADT as a parameter of some sort (or a ScopedValue, now that that is an option available to us), and even then, you would end up recreating the concept of a stack trace. Exceptions give you a stack trace, they throw and rewrap with less ceremony, and all while uncluttering your happy path, even more than ADT's. ADT's make everything, success and failure, a first class citizen, which, like you said, forces everyone to hold the exceptional cases front and center, regardless of how important they are. If that hurts your API's ergonomics, well, you opted into that.<br></div><div style="font-family:monospace" class="gmail_default"><br>> Error handling is hard, no matter how you slice<br>> it.<br><br>I see that much more now. Still, I am making excellent progress in mastering that, especially in these past few days.<br><br>Thank you for your time and help!<br>David Alayachew<br></div></div>
</blockquote></div></div></div>