Why does OCaml use exceptions instead of representing errors with Sum Types?

There are lots of case where exceptions are far superior to sum types, both in terms of readability and efficiency. And yes, it sometimes is safer to use exceptions.

Handling division by zero alone would be hell

The best example I can think of is also the simplest: / would be a total pain if it returned a sum type. A simple piece of code like:

let x = ( 4 / 2 ) + ( 3 / 2 ) (* OCaml code *)
let x' = match ( 4 / 2 ), ( 3 / 2 ) with
         | Some a, Some b -> Some ( a + b )
         | None, _ | _, None -> None (* Necessary code because of division by zero *)

Of course, this is an extreme case and an error monad can make that a lot easier (and monads are in fact about to be more usable in OCaml) but this also shows how sum types can lead to reduced efficiency. By the way, this is way exception can indeed be safer than sum types. Code readability is an incredibly important safety issue.

This would create a lot of dead code

There are a lot of cases when you know that no exception will be returned even though it could (array access within a for loop, division by a number you know isn't zero etc.). Most of the times, the compiler would notice that nothing wrong will happen and can remove the dead code, but not always. When that happens, an exception raising code will be lighter than a sum-type based code.

Adding an assert or a printf would require you to change your function signature

I don't have much to say more than that title. Adding a few debug instructions in your code would require you to change it. That can be what you want, but it would utterly break my personnal workflow and that of a lot of devs I know.

Retro-compatibility

The final reason to keep those exceptions is retro-compatibility. A lot of code out there relies on Hashtbl.find. Refactoring is easy in OCaml, but we're talking about a full ecosystem overhaul with potential bugs introduced and a certain loss of efficiency.


TL;DR; The main reason is performance. Reason number two is usability.

Performace

Wrapping a value into an option type (or the result type) requires an allocation and has its runtime cost. Basically, if you had a function returning an int and raising Not_found if nothing was found, then changing this function to int option will allocate a Some x value, which will create a boxed value occupying two words in your heap. This is in comparison with zero allocation in the version that used exceptions. Having this in a tight loop can drastically decrease the overall performance. Like 10 to 100 times, really.

Even if the returned value is already boxed, it will still introduce an extra box (a one word of overhead), and one layer of indirection.

Usability

In the non-total world, it soon becomes very obvious that non-totality is contagious and spreads through all your code. I.e., if your function has a division operation and you don't have exceptions to hush this fact, then you have to propagate the non-totality forward. Soon, you will end up with all functions having the ('a,'b) result and you will be using the Result monad to make your code manageable. But the Result Monad is nothing more than a reification of the exceptions, just slower and more awkward. Therefore we are back to the status quo.

Is there an ideal solution?

Apparently, yes. An exception is a particular case of a side effect of computation. The OCaml Multicore team is currently working on adding an effect system to OCaml in the style of the Eff programming language. Here is a talk and I've found some slides also. The idea is that you can have the benefits of two worlds - explicit type annotation of an effectful function (as with variants) and efficient representation with an ability to skip uninteresting effects (as with exceptions).

What to do right now?

While we, the common folks, are waiting for the effects to be delivered to OCaml, we still have to live with exceptions and variants. So what should we do? Below is my personal code of conduct, which I employ when I program in OCaml.

To handle the usability issue, I employ the rule - use exceptions for bugs and programmer errors. More explicitly, if a function has a checkable and clearly defined precondition, then its incorrect usage is a programmer error. If a program is broken it shouldn't run. Thus, use exceptions if the precondition is failed. A good example is the Array.init function, which fails if the size argument is negative. There is no good reason to use the result sum type to tell the user of the function, that it was using it incorrectly. The crucial moment with this rule is that the precondition should be checkable - and it means, that the check is fast and easy. I.e., host-exists or network-is-reachable is not a precondition.

To handle the performance issue, I'm trying to provide two interfaces to each non-total function, one which clearly raises (that should be stated in the name) and another using the result type as the return value. With the latter being implemented via the former.

E.g., something like, find_value_or_fail or (in using the Janesteet style, find_exn, and just the find.

In addition, I'm always trying to make my functions robust, by basically following the Internet Robustness Principle. Or, from the logic point of view, to make them stronger theories. In other words, it means that I'm trying to minimize the set of preconditions and provide reasonable behavior for all possible inputs. For example, you might find that the drastic division by zero has a well-defined meaning in the modular arithmetics, under which GCD and LCM will start to make sense as the divisibility lattice meet and join operations.

Our world is probably more total and complete than our theories as we usually don't see lots of exceptions around us :) So before raising an exception or indicating an error in some other way, think twice, is it an error or it is just an incompleteness of your theory.