Essentially, this is a variation on recommendation candidate #1, without polymorphic variants and with the addition of an obscure type for error-causing functions.
In a nutshell, a module conforming with this recommendation will
Assert_failure) has a suffix "_exn"Invalid_argument to represent invalid arguments or Assert_failure to represent internal errors
type ('a, 'b) may_fail (*obscure type, hiding the error-carrying mechanism*)
and
type ('a, 'b) status =
Success of 'a
| Error of 'b
may_fail and/or status'a option when there is no meaningful result but there is no actual error.The recommendation does not specify how these details should be presented.
According to this recommendation, function find of module List will could have the following signatures:
val find_exn: ('a -> bool) -> 'a list -> 'a
val find: ('a -> bool) -> 'a list -> 'a option
Both versions could coexist in the library.
Note that, when invoked,
find some_predicate some_list
may produce exceptions if some_predicate raises an exception.
Similarly, we may introduce
type parse_error_reasons =
Overflow
| SyntaxError
type parse_error =
{
source : string;
offset : int;
reason : parse_error_reasons;
}
val parse_int : string -> (int, parse_error) may_fail
This recommendation applies to module developers working on modules meant to be used in functional or mostly-functional settings. This recommendation specifies nothing about the internal implementation of these modules, only about how results and errors are presented to clients.
For instance, Map, List, Array, Hashtbl, Event would be subject to this recommendation: despite the fact that some of the most important functions of Array, Hashtbl or Event produce side-effects, these modules are often used in otherwise mostly-functional programs.
By opposition, functions of modules such as Unix or LablGl's GLDraw, are meant to be used mostly as a sequence of imperative operations. Consequently, these modules are not covered by this set of conventions.
By experience, checking the value of results at each step of mostly-imperative code is tedious and makes the code harder to read.
On the other hand, in mostly-functional settings, matching the result of an operation against patterns is quite common. If necessary, trivial functions may hide this error-checking either by using monadic-style or by raising exceptions.
This recommendation requires the adoption of a few standard types and functions. Tentatively, we will group them in a module Exceptionless and give them the following signature:
(**
The type of an operation which may either succeed and produce a result of
type 'a or fail and produce a result of type 'b.
*)
type ('a, 'b) may_fail
(**
The status of an operation.
*)
type ('a, 'b) status =
Success of 'a
| Error of 'b
(**
Produce a success
*)
val return : 'a -> ('a, 'b) may_fail
(**
Produce an error
*)
val throw : 'b -> ('a, 'b) may_fail
(**
Evaluate an expression and decode its status.
*)
val result : ('a, 'b) may_fail -> ('a, 'b) status
Additional functions may be added to this module to provide the ability to handle errors in a fully monadic manner or re-raise them. However, fully monadic error management is not the purpose of this recommendation.
The use of type ('a, 'b) may_fail gives the ability to read the type of errors which the evaluation of an expression call may cause. This type is obscure and de-coupled from ('a, 'b) status, so as to
Errorresult)We prefer a standardized type ('a, 'b) status to a set of polymorphic variants as using a simple variant type lets the compiler check that all errors are, indeed, handled.
Several prototypes for this module are already implemented and will be published online soon.
In a module conforming to this recommendation, every value of the module's signature guarantees the following points:
Assert_failure, the name of the function must end with "_exn"Invalid_argument, in case of an error made by the clientAssert_failure, in case of an internal inconsistency error('a, 'b) may_fail, where 'a is the type used to carry meaningful results and 'b is the type used carry details on the failuref accepts a function p as argument and p raises an exception, the flow of this exception should not be altered('a, 'b) may_fail is reserved for functions which may fail -- by opposition, functions which do not always have a meaningful result but cannot fail should use 'a option.This recommendation does not specify how details on the failure should be represented.
As mentioned above, any function may raise Assert_failure.
The rationale for this decision is twofold. Firstly, Assert_failures are raised as a consequence of failed assert, which in turn is used to warn of a programmer error. Assert_failures are not meant to be caught, except perhaps by a toplevel emergency exit routine. Consequently, it cannot be reasonably expected for the rest of the program to deal with these errors.
Secondly, these exceptions are raised because the function reached a state which should be impossible by design. One may not expect a programmer to be able to predict his own errors and both deal with them cleanly (by applying exceptionless error management) and not fix them (by letting the assertion fail).
A consequence of this is that assert is not necessarily the right mechanism.
As mentioned above, whenever a function f accepts a function p as argument and p raises an exception, the flow of this exception should not be altered. In other words, in this case, f p may raise an exception, even though the name of f does not end with _exn.
The rationale for this decision is the following:
p which may raise an exception, there is probably a good reason for passing that function to f rather than an exceptionless counterpartp raises an exception, it's unclear what f should produce as result: None ? Error `CaughtException ? Error (`CaughtException e), where e is the exception ? None of these answers is satisfying.f even though the source code of f contains no raise or assert is tedious, error-prone and not automatizable unless a satisfying answer to the previous point is found.We are well aware that exceptions are not always avoidable and that they may be useful in some circumstances. This is the reason why the recommendation does not wish to force module authors to completely remove exception mechanisms. However, for modules covered by this recommendation, the default error-management mechanism should be exceptionless error management.
Most functions should therefore not raise exceptions other than Assert_failure. If it is necessary to write a function which may raise exceptions, an exceptionless counterpart should be provided.
Usual common-sense rules wrt data structures should apply:
Not finding anything in a list is not considered an error. Therefore, we will tend to write
# let find p l = try Some (List.find p l) with Not_found -> None
val find: ('a -> bool) -> 'a list -> 'a option
We may also decide to wrap the old find function with the new naming conventions:
# let find_exn = List.find
val find_exn: ('a -> bool) -> 'a list -> 'a
Dividing by zero is a client error and may therefore raise an exception of constructor Invalid_arg.
# let divide_exn x y = if y = 0 then raise ( Invalid_arg "division by zero" ) else x / y val divide_exn: int -> int -> int
An alternative approach would be to consider DivisionByZero as an Error:
# type division_by_zero = [ DivisionByZero ] # let divide x y = if y = 0 then throw DivisionByZero else return ( x / y ) val divide: int -> int -> (int, division_by_zero) may_fail
Here, we had to replace raise with throw and to specify return before the result.
A simple implementation of integers, ignoring sign, parsing may be written as follows:
open Exceptionless
type parse_error_reasons =
Overflow
| SyntaxError
type parse_error =
{
source : string;
offset : int;
reason : parse_error_reasons;
}
let parse_int s =
let rec aux i power acc =
if i < 0 then return acc
else match s.[i] with
| '0' .. '9' as d ->
let new_acc = ( acc + power * ( int_of_char d - int_of_char '0' ) ) in
if new_acc >= 0 then (*Check if sign has suddenly changed*)
aux ( i - 1 ) ( power * 10 ) new_acc
else
throw { source = s ; offset = i ; reason = Overflow }
| _ -> throw { source = s ; offset = i ; reason = SyntaxError }
in
aux (String.length s - 1) 1 0
Note that, despite the Java-like naming of functions, we returned the result of return acc throw { source = s ; offset = i ; reason = Overflow }. The implementation of module Error may decide to implement these functions using exceptions or the regular flow of informations.
Also note that the length of the function is similar to what we would have written had we used exceptions: we have added a return and reformulated two raise in terms of throw.
Once we have defined this function, we may use it as follows:
let attempt_parsing s =
match result (parse_int s) with
| Success i ->
Printf.printf "Successfully parsed %i\n" i
| Error ({reason = Overflow} as e) ->
Printf.printf "Overflow error when parsing \"%s\"\n" e.source
| Error ({reason = SyntaxError} as e) ->
Printf.printf "Syntax error at offset %d when parsing \"%s\"\n"
e.offset
e.source
let _ = attempt_parsing "123456"
let _ = attempt_parsing "12345678989123423148716259823452938652"
let _ = attempt_parsing "-12345"
I mostly agree. However I think:
type ('a,'b) result = Ret of 'a | Err of 'b
let ret x = Ret x
let err x = Err x
let apply f x = try Ret(f x) with e -> Err e
-- Berke