Sealed Types vs Exceptions?
Brian Goetz
brian.goetz at oracle.com
Tue Dec 5 01:21:17 UTC 2023
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.)
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 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.
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.
Error handling is hard, no matter how you slice it.
On 12/4/2023 7:05 PM, David Alayachew wrote:
> Hello Amber Dev TeaM,
>
> I learned a lot about Exceptions in the previous discussion above, so
> I figured I'd ask this question as well -- when does it make sense to
> handle exceptional cases via a Sealed Type (DivisionResult ====>
> Success(double answer) || DivideByZero()) vs an Exception
> (DivideByZeroException)?
>
> The only difference I can see is that an Exception gives you debugging
> details (line number, stack trace) that would be very difficult for a
> Sealed Type to attain. And if that is the key difference, doesn't that
> sort of imply that we should opt into the more info-rich source of
> information wherever possible? After all, debugging is hard enough and
> more info is to everyone's benefit, right?
>
> And most of the extra info is static (line numbers don't change). I'll
> avoid performance as a reason, as I don't understand the mechanics of
> what makes one faster or slower.
>
> Thank you all for your time and help!
> David Alayachew
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20231204/7b9891d9/attachment.htm>
More information about the amber-dev
mailing list