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.