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

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".

1 Like

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

2 Likes

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

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.

2 Likes

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 ()

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.

3 Likes

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.

2 Likes

Sorry, I see code bloat where you see purity.

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.

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.

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.

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.

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.

2 Likes

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.

1 Like

what are the arguments against putting escaping exceptions in a signature? (or via the new commented attribute extension)

I can imagine a scheme where such are automatically written into .cmi interfaces but are optional to write explicitly in the .mli, and when programming defensively for instance, in the presence of 3rd party code (or otherwise), specifying an empty escape list on all one’s functions for the compiler to check (or a compiler flag to enforce compliance)

the compiler leaves out handled exceptions from the .cmi and
in the case of exceptions known to the programmer not to be raisable (due to program logic) a ‘ignore Invalid_argument’ like statement could be added (or attribute extension), which also removes it from the .cmi.
this ignored exception still could be raised (fallible programmer) but if the application is targetting continuous running in the presence of software errors it’s going to have catch-all handlers in any case for logging/alerting/recovery.

so although the possibility to know all escaping exceptions is not in fact offered, it is not a disadvantage in practice, while the advantages are freedom from specifiying escapee exceptions in interfaces (unless desired or relevant) but having the facility to detect them at compile time and react accordingly.

Is your idea being similar to effect system presented by Leo White in his talk?

I can’t hear enough of what he’s saying to make sense of it.
I wasn’t aware this had anything to do with expressing exceptions through the type system. I’m looking through some slide decks on it but have not yet found the above connection (subtyping extension to MLsub implementation though)
Have you a more specific reference that covers it?

Have you a more specific reference that covers it?

Not really, no

I think I can forget about my hack!

Although the ocamllabs tutorial for effects in multicore doesn’t mention effects typing

…this slide deck:
http://gallium.inria.fr/seminaires/transparents/20161205.Leo.White.pdf

refers to a ‘type & effect’ system as the next logical step from algebraic effects.
It appears there’s a problem with row polymorphism but a ‘compromise’ is given in derivation detail.
Intriguingly the code samples are in teletype - an experimental implementation?

But it seems multicore is not supported on my system so unable to verify.
Is it implemented in multicore?

1 Like

… and here, all along, is the experimental implementation of ocaml with a type system tracking effects:

meaning exceptions can be tracked through the type system!
I think the problem may be it’s non-backward compatibilty but I hope it eventually gets into the distro on a switch of some sort anyway.