Specific reason for not embracing the use of exceptions for error propagation?


#21

For starters it would help not to see it as “contamination”. You have a computation whose result maybe not only be a valid value, but also an error. That creates a complex type.

Converting an Error.t or Result.t to an exception, seems to get the worst of both approaches, unless you really have to do such a conversion due to some API or whatever.

Generally you need to either propagate the error to another area of the code base that knows how to deal with such error (e.g. division by zero could request the user to enter it again). Monads and functors help you do that (e.g. >>=).

Another thing you could do is have a mandatory or optional function parameter which knows how to deal with various errors (e.g. division ~error_handler a b). I said this in the beginning of the thread… this really depends on the specifics of each problem and algorithm.

If you get a chance try both approaches and see what happens in the long run.


#22

Exactly. That’s what exception propagation does. Without monads or functors.

Been there, done that. The most usable pattern was Http_exn.not_found "no such user account".


#23

Yes, and at the cost of sacrificing guarantees from the type system.


#24

(If that’s seems like the right trade off-for your use case, then by all means use exceptions.)


#25

I think we need to distinguish the case when a function cannot be evaluated at a point because the arguments are outside the domain of the function and the case when evaluation of the function at a possibly valid point of it’s domains cannot be carried due to external factors. Examples of external factors would be external services not being available or misbehaving and resource exhaustion. I would only consider the latter case of errors true runtime errors, and a prime use case for result, since such errors are outside the control of the code, and generally should be handled.

What about domain errors then? In general I would tend to classify these as bugs, but there may be cases where it’s safer or more convenient to use result or option rather than raising exceptions. Some examples where I would prefer exceptions:

  • Division by zero. The code which passed the zero is most likely broken. If the argument is a float, this is easily seen by adding a noise to the divisor. If the program relies on catching division by zero, it will instead receive huge numbers which may invalidate the result.
  • More generally all floating and integer point functions which correspond to mathematical functions. E.g. there is a good practise in analysis to carefully specify domains, and if the code is correct it stays within those domains.
  • I think the practise of making sure to stay within the domain is preferable for pure functions as far as feasible (see below). A symptom of a result which should have been an exception is that the users can clearly see something is wrong and terminates the Error case with an assert false.

But what about the danger that the code fails with an uncaught exception? Well, isn’t there an even greater risk that the code just makes a wrong calculation on some parts of its domain? Note in contrast, that true runtime errors can occur no matter how correct the code is, so it makes a lot more sense to force the caller to deal with it.

Still, one reason to use result for domain errors may be that the structure of the part of the domain on which the function is defined is too complex for the caller to be expected to deal with. I typical example would be conversions and parsers. If inputs come from external sources, it should be checked, but the easiest way to check the input may be to actually try to parse it.

What about find then? This can be seen as another case of a complex domain, since there is a non-trivial dependency between the container and the search key (though of course one can use mem to do the check). But another resolution of this case, which I think is more elegant, is to not consider a negative lookup to be an error at all, but to rather expose the map as an explicitly partial function. That is, to let find return an option instead of a result. The justification for this view would be if uncaught Not_found exceptions tend to indicate that the code it fact needs a representation of a partial function.


#26

Which guarantees are you referring to?

In the following code (from earlier message), the type system does not tell you which function body might contain a bug because they all return Or_error.t instead of just the sensitive function divide:

let divide a b =
  if b = 0 then
    Error (Error.of_string "Division by zero")
  else
    Ok (a / b)

let f1 () =
  divide 0 0

let f2 () =
  f1 ()

#27

Maybe we need a slightly bigger example than division. A while ago I have explored a seemingly simple exercise in error handling:

Create a directory with a given set of permissions and ownership. And if it exists already, fix permissions and ownership when necessary or fail otherwise while giving a good reason.

The functions in the Unix module return exceptions but I found it easier to implement this using an error monad. In particular, I wanted to have sensible error messages. Maybe this can be also done with exceptions and I would be interested to read about it.

My writeup is here: Making a Directory in OCaml.


#28

Assume

  • you have a function f : a -> b
  • the code of f is “pure” (which I roughly take to mean it does not use exceptions, and all functions and operators it uses are also pure)

then the type system guarantees that f will either (i) terminate with a value of type b, or (ii) diverge.
If on the other hand f is “impure” (i.e., it uses exceptions) then there is a third possibility: (iii) f may throw an arbitrary exception.

You could imagine an even stronger notion of “purity” where the type system rules out case (ii) as well (and in fact such type systems are used in proof assistants like Coq and Isabelle). The stronger your notion of “purity”, the stronger the guarantees your type system can provide. Another popular notion of “purity” (for example in Haskell) is no state & no IO. Then the type system can rule out nondeterminism.


#29

Sorry, I see code bloat where you see purity.


#30

What kind of guarantee is that? Removing exceptions from the system and having every function return Ok_error.t adds no information to the program. One form of the program can be trivially converted to the other, which is why we prefer the form that’s shorter, i.e. the one using exceptions.

This leaves room for using Or_error.t on some functions, while still using exceptions. See earlier posts.


#31

I did not make any judgment on whether purity is a good thing or not. I just gave you the facts. You can do with them whatever floats your boat.


#32

The trick is that in this translation, you can leave pure functions unmodifiee and have them return exactly the same thing as before, not an Or_error.t. Only impure functions have to be modified in the way you describe. And now you get the extra benefits that I described above. (Of course if you have no pure functions, you indeed win nothing.)

EDIT: In particular, you may make your “main function” pure by handling potential Or_error.t’s, if there are any. Then you’re guaranteed to never see uncaught exceptions.


#33

Just for the record, I have no problem with impurity and have never used Or_error.t or Result.t before because it doesnt seem worth it for my purposes.


#34

What is the purpose of f2? If f2 is a public interface that provides the capability of dividing integers, then returning an Or_error.t is intended because it could fail. This example might be too small and too abstract to gain any conclusion. And for your earlier post:

If the only function that can fail is divide, then yes. But imagine f1 (again, I’m afraid that using an imaginary function name like this might make it harder to deliver my point) calls other functions that can also fail. You would then end up with:

let f1 () =
  match divide 0 0 with
  | Error e -> Error.raise e
  | Ok x ->
    match do_something x with
    | Error e -> Error.raise e
    | Ok (y, z) ->
      match do_another_thing z with
      | Error e -> Error.raise e
      | Ok fin -> fin + y

Monadic constructs such as bind (>>=) can help simplifying this kind of code into:

let f1 () =
  let final_result =
    divide 0 0 >>= fun x ->
    do_something x >>= fun (y, z) ->
    do_another_thing z >>= fun fin ->
    fin + y
  in
  match final_result with
  | Error e -> Error.raise e
  | Ok v -> v

So it’s the choice of whether to handle or to propagate, and determining when to do which is deeply related to the nature and contract of the functions and what you’re trying to do.


#35

I personally like and use OCaml exceptions (sometimes).
And I don’t even mark all incriminated functions with _exn.
To me, an exception is exceptional, and if I encounter an unexpected one in production, I will handle it in the next version of the software.
That being said, I don’t write soft-real-time daemons which are supposed to work round the clock.