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