Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I've read https://stackoverflow.com/a/12161946/ which somewhat addresses OCaml exceptions in the context of performance and mentions that one might use exceptions to intentionally manipulate control flow.

However, I'd like to know the rationale behind adding exceptions to a language with first class Sum Types, from a language design/historical perspective.

My understanding (and please correct me if I'm mistaken), is that exceptions in OCaml subvert the type system and thereby make it harder to reason about the specific state of a program. Unlike with matches on sum types, the compiler will not check if all possible error cases are handled, which could become a problem especially if a modification to a library function introduces a new error state. This is why, for example, the Zig programming language enforces error handling and provides a compiler-enforced construct for checking all possible error cases (https://ziglang.org/#A-fresh-take-on-error-handling).

Given the above, and given that there might be situations where bypassing multiple stack frames could be useful, I could imagine a different language construct (perhaps something akin to a labeled break) with that role that doesn't have a semantic association with error handling.

Are there any (many?) situations where exceptions to handle errors are superior to explicit, compiler-checked error handling?

I especially don't understand something like Hashtbl.find throwing an exception. Given that Hashtbl.find_opt was introduced a few years ago, does this represent some shift in the direction of the standard library design while not breaking existing programs?

Are exceptions in OCaml and the standard library an artifact of the time when OCaml was designed (eg were exceptions popular at the time/were their consequences not fully understood), and/or is there a good reason for the language to have exceptions?

like image 242
Julian Ceipek Avatar asked Jul 11 '19 04:07

Julian Ceipek


People also ask

Does OCaml have exceptions?

OCaml has exception handling to deal with exceptions, application-level errors, out-of-band data and the like. Exceptions are a very dynamic construct, but there is a third-party application that performs a static analysis of your use of exceptions and can find inconsistencies and errors at compile time!

What is Failwith in OCaml?

The simplest one is failwith , which could be defined as follows: let failwith msg = raise (Failure msg);; val failwith : string -> 'a = <fun> OCaml. There are several other useful functions for raising exceptions, which can be found in the API documentation for the Common and Exn modules in Base.

What is OK in OCaml?

A value Ok x means that the computation succeeded with x , and a value Error e means that it failed. Pattern matching can be used to deal with both cases, as with any other sum type.


2 Answers

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.

like image 148
ivg Avatar answered Sep 19 '22 15:09

ivg


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.

like image 34
PatJ Avatar answered Sep 19 '22 15:09

PatJ