Previous Contents Next

Typing, domain of definition, and exceptions

The inferred type of a function corresponds to a subset of its domain of definition. Just because a function takes a parameter of type int doesn't mean it will know how to compute a value for all integers passed as parameters. In general this problem is dealt with using Objective CAML's exception mechanism. Raising an exception results in a computational interruption which can be intercepted and handled by the program. For this to happen program execution must have registered an exception handler before the computation of the expression which raises this exception.

Partial functions and exceptions

The domain of definition of a function corresponds to the set of values on which the function carries out its computation. There are many mathematical functions which are partial; we might mention division or taking the natural log. This problem also arises for functions which manipulate more complex data structures. Indeed, what is the result of computing the first element of an empty list? In the same way, evaluation of the factorial function on a negative integer can lead to an infinite recursion.

Several exceptional situations may arise during execution of a program, for example an attempt to divide by zero. Trying to divide a number by zero will provoke at best a program halt, at worst an inconsistent machine state. The safety of a programming language comes from the guarantee that such a situation will not arise for these particular cases. Exceptions are a way of responding to them.

Division of 1 by 0 will cause a specific exception to be raised:

# 1/0;;
Uncaught exception: Division_by_zero
The message Uncaught exception: Division_by_zero indicates on the one hand that the Division_by_zero exception has been raised, and on the other hand that it has not been handled. This exception is among the core declarations of the language.

Often, the type of a function does not correspond to its domain of definition when a pattern-matching is not exhaustive, that is, when it does not match all the cases of a given expression. To prevent such an error, Objective CAML prints a message in such a case.

# let head l = match l with h::t -> h ;;
Characters 14-36:
Warning: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
[]
val head : 'a list -> 'a = <fun>


If the programmer nevertheless keeps the incomplete definition, Objective CAML will use the exception mechanism in the case of an erroneous call to the partial function:

# head [] ;;
Uncaught exception: Match_failure("", 14, 36)


Finally, we have already met with another predefined exception: Failure. It takes an argument of type string. One can raise this exception using the function failwith. We can use it in this way to complete the definition of our head:

# let head = function
[] -> failwith "Empty list"
| h::t -> h;;
val head : 'a list -> 'a = <fun>
# head [] ;;
Uncaught exception: Failure("Empty list")


Definition of an exception

In Objective CAML, exceptions belong to a predefined type exn. This type is very special since it is an extensible sum type: the set of values of the type can be extended by declaring new constructors9. This detail lets users define their own exceptions by adding new constructors to the type exn.

The syntax of an exception declaration is as follows:

Syntax


exception Name ;;
or

Syntax


exception Name of t ;;
Here are some examples of exception declarations:

# exception MY_EXN;;
exception MY_EXN
# MY_EXN;;
- : exn = MY_EXN
# exception Depth of int;;
exception Depth of int
# Depth 4;;
- : exn = Depth(4)
Thus an exception is a full-fledged language value.

Warning


The names of exceptions are constructors. So they necessarily begin with a capital letter.

# exception lowercase ;;
Characters 11-20:
Syntax error


Warning


Exceptions are monomorphic: they do not have type parameters in the declaration of the type of their argument.

# exception Value of 'a ;;
Characters 20-22:
Unbound type parameter 'a
A polymorphic exception would permit the definition of functions with an arbitrary return type as we will see further on, page ??.

Raising an exception

The function raise is a primitive function of the language. It takes an exception as an argument and has a completely polymorphic return type.

# raise ;;
- : exn -> 'a = <fun>
# raise MY_EXN;;
Uncaught exception: MY_EXN
# 1+(raise MY_EXN);;
Uncaught exception: MY_EXN
# raise (Depth 4);;
Uncaught exception: Depth(4)
It is not possible to write the function raise in Objective CAML. It must be predefined.

Exception handling

The whole point of raising exceptions lies in the ability to handle them and to direct the sequence of computation according to the value of the exception raised. The order of evaluation of an expression thus becomes important for determining which exception is raised. We are leaving the purely functional context, and entering a domain where the order of evaluation of arguments can change the result of a computation, as will be discussed in the following chapter (see page ??).

The following syntactic construct, which computes the value of an expression, permits the handling of an exception raised during this computation:

Syntax


try expr with
| p1 -> expr1
:
| pn -> exprn

If the evaluation of expr does not raise any exception, then the result is that of the evaluation of expr. Otherwise, the value of the exception which was raised is pattern-matched; the value of the expression corresponding to the first matching pattern is returned. If none of the patterns corresponds to the value of the exception then the latter is propagated up to the next outer try-with entered during the execution of the program. Thus pattern matching an exception is always considered to be exhaustive. Implicitly, the last pattern is | e -> raise e. If no matching exception handler is found in the program, the system itself takes charge of intercepting the exception and terminates the program while printing an error message.

One must not confuse computing an exception (that is, a value of type exn) with raising an exception which causes computation to be interrupted. An exception being a value like others, it can be returned as the result of a function.

# let return x = Failure x ;;
val return : string -> exn = <fun>
# return "test" ;;
- : exn = Failure("test")
# let my_raise x = raise (Failure x) ;;
val my_raise : string -> 'a = <fun>
# my_raise "test" ;;
Uncaught exception: Failure("test")
We note that applying my_raise does not return any value while applying return returns one of type exn.

Computing with exceptions

Beyond their use for handling exceptional values, exceptions also support a specific programming style and can be the source of optimizations. The following example finds the product of all the elements of a list of integers. We use an exception to interrupt traversal of the list and return the value 0 when we encounter it.

# exception Found_zero ;;
exception Found_zero
# let rec mult_rec l = match l with
[] -> 1
| 0 :: _ -> raise Found_zero
| n :: x -> n * (mult_rec x) ;;
val mult_rec : int list -> int = <fun>
# let mult_list l =
try mult_rec l with Found_zero -> 0 ;;
val mult_list : int list -> int = <fun>
# mult_list [1;2;3;0;5;6] ;;
- : int = 0
So all the computations standing by, namely the multiplications by n which follow each of the recursive calls, are abandoned. After encountering raise, computation resumes from the pattern-matching under with.


Previous Contents Next