Exceptions are the best way to hundle errors in your code and decide what to do when your encounter one of them, if your have already worked with .net, java, python or a lot of other programming languages, you may already know what are exceptions and why they are out there, if you don't yet let's just see a small problem fo why exceptions are used!
consider the following function that returns the head (first element) of a list:
# let head l = match l with [] -> ??? | x::_ -> x;;
val head : 'a list -> 'a = <fun>
what would return this function when the list is empty []?
as a first solution we can use the type 'a option defined as follows :
type 'a option = None | Some of 'a
then the funtion head would be like the following
# let head l = match l with [] -> None | x::_ -> Some (x);;
val head : 'a list -> 'a option = <fun>
This function works well, but at this point we lost the function signature, we don't return the type of the list's elements anymore but the type option, which makes the code not the best way to go since OCAML is everything about types.
Exceptions :
In such cases exceptions are the best solution, and if you are not familiar with functional programming, you will see some magic here :).
conside the same function as before but using exceptions this time :
# let head l = match l with [] -> raise Not_found | x::_ -> x;;
val head : 'a list -> 'a =<fun>
As you see the function signature is exactly what we want (a beatiful polymorphic signature!). if we pass an empty list the function will raise the exception Not_found and if there is no hundler for the an exception (just bellow) the program will fail and stop execution.
If you are a good functional programmer you must be asking yourself: "What type is the expression raise Not_found"? as in general cases a function must have a single signature, which means the raise Not_found have the same type of x in the above example!
First lets see what is the type of the raise function, typing this in ocaml terminal reveals it :
# raise ;;
- : exn -> 'a = <fun>
As you can notice the raise function accept a exn type and return an 'a type, which means
raise Not_found return an the polymorphic 'a type which totally is acceptable in the above function as you already know thanks to the OCAML generic types.
Exception have the exn type, and are declared with the exception keyword as follows
# exception My_exception;;
exception My_exception
and the type is exn as mentionned before :
# My_exception;;
- : exn = My_exception
We can also declare exceptions that accept arguments like the sum types do, but becarreful no polymorphic types are accepted in this context:
exception <name>
exception <name> of <parameters>
# exception My_exception of int * int * string;;
exception My_exception of int * int * string
# My_exception (5,9,"exceptions are so cool");;
- : exn = My_exception (5, 9, "exceptions are so cool")
the exn type is so special in OCAML since it's a dynamic type, which means that when you declare a new exception you are actually adding it as a member of the exn sum type which already contains some default exeptions like Not_found, Failure ..., and because it's a sum type the first letter of the exception name must be a capital letter.
As we saw the raise function allow us to raise or throw (like .net people like to call) an exception, the execution stops immediatly in the point of the raise call and the excution flow goes immediatly to the first hundler for the specific exception, if no hundler is present the program breaks down.
handling exceptions:
The try ... with allow us to hundle exceptions like follows:
try with
Exception1 -> .....
Exception2 -> .... raise AnotherException
....
ExceprionN -> ...
Consider the following example :
# exception E of int;;
exception E of int
# let f x = if x=10 then raise (E 10) else 10/x;;
val f : int -> int = <fun>
# try f 10 with
Division_by_zero -> 0
| E x -> x;;
- : int = 10
# try f 0 with
Division_by_zero -> 0
| E x -> x;;
- : int = 0
# try f 3 with
Division_by_zero -> 0
| E x -> x;;
- : int = 3
As you can see the different errors are captured, and are very nicelly hundeled, this way you are sure your program will never break down because of some other unhundeled errors (as C programmers know and their ugly error hundling system).
A little test :D
Let's suppose we have a list that we want to calculate the multiply of all it's elements, and because wa want the best performance we want to stop when we encounter the first 0.
# let l=[1;6;3;7;2;0;4;7;.......................];;
A natural approach would be like follows:
let rec mult l=
match l with
[] -> 1
| hd::tl -> hd * (mult tl);;
val mult : int list -> int = <fun>
As you expect this would iterate all the list even when a zero is found, a simple workaround would be:
# let rec mult l=
match l with
[] -> 1
| 0::_ -> 0
| hd::tl -> hd * (mult tl);;
val mult : int list -> int = <fun>
This one do stop when the first 0 is encountered by will do the multiplying back of the elements found before anyway, means (0 * 2 * 7 * 3 * ....) so if 0 is in position 15 it will do 2*15 iteration.
As a last solution we can interrupt the execution with and exception, but here are two versions, can you tell which one is more accurate?
# exception Zero_found;;
exception Zero_found
version 1 :
# let rec mult l =
match l with
[] -> 1
| 0::_ -> raise Zero_found
| hd::tl -> try (hd * mult tl) with Zero_found -> 0;;
val mult : int list -> int = <fun>
version 2 :
# let mult l =
let rec m lst =
match lst with
[] -> 1
| 0::_ -> raise Zero_found
| hd::tl -> hd * (m tl)
in try m l with Zero_found -> 0;;
val mult : int list -> int = <fun>
Exceptions can also be used to speed calculations and interrupt itterator like List.fold_left...
The answer for the question above is : version 2 is the more accurate since it stops when the first 0 is found, but the first just skip one step when 0 is found and countinue the multimplying back, means (0 -here 2 is skipped because of the exception- * 7 * ....).