Sealed Types vs Exceptions?

David Alayachew davidalayachew at gmail.com
Wed Dec 6 02:40:44 UTC 2023


Hello Brian,

Thank you for your response!

> Error handling is hard, no matter how you slice
> it. (See
> https://joeduffyblog.com/2016/02/07/the-error-model/
> for a mature analysis of all the options.)

That was a really nice read! And yes, it was definitely comprehensive, no
question.

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.

> The benefit of using algebraic data types
> (e.g., the Either or Try monads) for error
> handling is _uniformity_.  Type systems such as
> the simply typed lambda calculus (Peirce, Ch9)
> say that if we have types T and U, then the
> function type `T -> U` is also a type. Such
> function types are well behaved, for example
> they can be composed:
> if f : T -> U and g : U -> V, then g.f : T -> V
>
> A Java method
>
>     static int length(String s)
>
> can be viewed as a function String -> int.  But
> what about a Java method
>
>     static int parseInt(String s) throws NumberFormatException
>
> ?  This is a _partial function_; for some
> inputs, parseInt() does not produce an output,
> instead it produces an effect (throwing an
> exception.)  Type systems that describe partial
> or effectful computations are significantly
> more complicated and less well-behaved.
>
> Modeling a division result as in the following
> Haskell data type:
>
>     data DivResult = Success Double | Failure
>
> means that arithmetic operations are again total:
>
>     DivResult divide(double a, double b)
>
> This has many benefits, in that it becomes
> impossible to ignore a failure, and the
> operation is a total function from double x
> double to DivResult, rather than a partial
> function from double x double to double.  One
> can represent the success-or-failure result
> with a single, first-class value, which means I
> can pass the result to other code and let it
> distinguish between success and failure; I
> don't have to deal with the failure as a
> side-effect in the frame or stack extent in
> which it was raised.  The common complaints
> about "lambdas don't work well with exceptions"
> comes from the fact that lambdas want to be
> functions, but unless their type (modeled in
> Java with functional interfaces) accounts for
> the possibility of the exception, we have no
> way to tunnel the exception from the lambda
> frame to the invoking frame.

So, this was interesting to read. And they said something similar in the
article that you linked.

On the one hand, that uniformity allows us to cleanly and easily
latch/compose things in a happy path sort of way.

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).

My reasoning for this is from an article by Alexis King (Lexi Lambda) --
"Parse, don't validate" (
https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/). 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.

> It is indeed true that exceptions carry more
> information, and that information comes at a
> cost -- both a runtime cost (exceptions are
> expensive to create and have significant memory
> cost) and a user-model cost (exceptions are
> constraining to deal with, and often we just
> throw up our hands, log them, and move on.)
> On the other hand, algebraic data types have
> their own costs -- wrapping result
> success/failure in a monadic carrier intrudes
> on API types and on code that consumes results.

Ok, cool. So it's a trade off between performance and information.

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?

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.

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.

> Error handling is hard, no matter how you slice
> it.

I see that much more now. Still, I am making excellent progress in
mastering that, especially in these past few days.

Thank you for your time and help!
David Alayachew
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20231205/f86f6eb2/attachment-0001.htm>


More information about the amber-dev mailing list