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 point of view of Mathematical Logic, 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.