Exceptions
Source
John Whitington, OCaml from the Very Beginning, Chapter 7: When Things Go Wrong
Contents
Question 1
Write a function
smallest
which returns the smallest positive element of a list of integers. If there is no positive element, it should raise the built-inNot_found
exception.
My solution
Designed to use filtered lists of data as input to solve this particular problem.
Find the smallest element of a list.
1
2
3
4
5
6
let smaller x y = if x <= y then x else y
let rec smallest l = match l with
[] -> raise (Invalid_argument "Empty list")
| h::[] -> h
| h::x::t -> smallest ((smaller h x)::t)
Filter out non-positive elements.
1
2
3
4
5
6
7
let rec filter f l = match l with
[] -> []
| h::t -> if f h then h::(filter f t) else filter f t
let filter_pos l = match filter (fun x -> x > 0) l with
[] -> raise Not_found (* Exception specified by the problem *)
| h::t -> h::t
Solution.
1
let smallest_pos l = smallest (filter_pos l)
Tests.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let _ =
(* Test `smallest` by itself. *)
assert (-1 = smallest [-1; 1]);
assert (0 = smallest [0; 1]);
assert (1 = smallest [2; 1]);
(* Test filtered lists including valid data. *)
assert (1 = smallest_pos [1]);
assert (1 = smallest_pos [1; 2]);
assert (1 = smallest_pos [2; 1]);
assert (1 = smallest_pos [-2; 1]);
assert (2 = smallest_pos [2; -1]);
assert (1 = smallest_pos [1; 2; 3]);
assert (1 = smallest_pos [3; 2; 1]);
assert (2 = smallest_pos [3; -2; 3; 7; -1; 0; 8; 2; -2; -6]);
;;
Test exceptions.
1
2
3
4
5
6
7
8
9
10
11
12
13
(* Improvised test of raised exception: https://stackoverflow.com/q/75077670 *)
let throws_exception f = match f () with
| exception _ -> true
| _ -> false
let _ =
(* Test filtered lists including no valid data. *)
assert (throws_exception (fun () -> smallest_pos []));
assert (throws_exception (fun () -> smallest_pos [0]));
assert (throws_exception (fun () -> smallest_pos [-1]));
let l = [-1; 0; -3; -5; -7; -12; 0; -1; -2] in
assert (throws_exception (fun () -> smallest_pos l));
;;
See exception messages displayed in the console.
1
2
3
4
5
6
(*
let _ = smallest [];; (* Invalid argument *)
let _ = smallest_pos [];; (* Not found *)
let _ = smallest_pos [0];; (* Not found *)
let _ = smallest_pos [-1];; (* Not found *)
*)
Solution provided in book
Cycles curr
and a found
flag to track state. An approach I made an
effort to avoid. It has a bit of an imperative smell.
1
2
3
4
5
6
7
8
9
10
11
12
let rec smallest_inner curr found l =
match l with
[] ->
if found then curr else raise Not_found
| h::t ->
if h > 0 && h < curr
then smallest_inner h true t
else smallest_inner curr found t
let smallest l = smallest_inner max_int false l
let _ = assert (2 = smallest [3; -2; 3; 7; -1; 0; 8; 2; -2; -6]);;
I was relieved to find that there did not exist (at least in this book) an obviously more compact and elegant solution than my own.
For the first time in working through this book, I find my own solution easier to read and otherwise more elegant. However, it does atomize the problem more extensively, which could be a disadvantage in another context.
I also prefer the logic of treating an empty list as an
Invalid_argument
in my solution (because there is simply no good
reason to ever use this function with an empty list), and reserving
Not_found
for the actual processing of a list with one or more
elements. Not_found
implies that there was something to be found, and
in my solution, that’s appropriate only when it has been verified that
there is in fact something (rather than nothing) to be found.
(On the other hand, there’s no intrinsic reason to treat an empty list as invalid data. So the question of the logic here is an open one in the end.)
Question 2
My solution
Write another function
smallest_or_zero
which uses the smallest function but ifNot_found
is raised, returns zero.
1
2
3
4
5
6
7
let smallest_pos_or_zero l =
try smallest (filter_pos l) with Not_found -> 0
let _ =
assert (0 = smallest_pos_or_zero [0; -1; -2; -3]);
assert (2 = smallest_pos_or_zero [3; -2; 3; 7; -1; 0; 8; 2; -2; -6]);
;;
Solution provided in book
…is identical to my own.
1
let smallest_or_zero l = try smallest l with Not_found -> 0
Question 3
Write an exception definition and a function which calculates the largest integer smaller than or equal to the square root of a given integer. If the argument is negative, the exception should be raised.
My solution
Using only Module Stdlib
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
exception NegativeArgument
let sqrt_int x =
if x < 0 then raise NegativeArgument
else int_of_float(sqrt (float_of_int x))
let _ =
assert (throws_exception (fun () -> sqrt_int (-1)));
assert (0 = sqrt_int 0);
assert (1 = sqrt_int 1);
assert (1 = sqrt_int 2);
assert (2 = sqrt_int 4);
assert (2 = sqrt_int 5);
assert (2 = sqrt_int 8);
assert (3 = sqrt_int 9);
;;
Solution provided in book
I did think it was odd that the problem appeared to be asking for an
implementation of sqrt
, which is not a trivial task. That isn’t the
case. Clearly I misunderstood the logic of the problem. Still, I think
its wording is susceptible to misunderstanding.
1
2
3
4
5
6
exception Complex
let rec sqrt_inner x n =
if x * x > n then x - 1 else sqrt_inner (x + 1) n
let sqrt n = if n < 0 then raise Complex else sqrt_inner 1 n
Question 4
Write another function which uses the previous one, but handles the exception, and simply returns zero when a suitable integer cannot be found.
My solution
Adapting the book’s solution by wrapping sqrt
in an exception handler.
1
2
3
let sqrt_handled n = try sqrt n with Complex -> 0
let _ = assert (0 = sqrt_handled (-1));;
Solution given in book
Is identical to my own.
Question 5
Comment on the merits and demerits of exceptions as a method for dealing with exceptional situations, in contrast to returning a special value to indicate an error (such as
-1
for a function normally returning a positive number).
My solution
Advantages of exceptions: standardized exceptions carry all of the advantages of standardization; custom exceptions require some interpretation but otherwise carry some of the advantages of standardization.
Disadvantages of exceptions: they need handling; cognitive overhead of decisions regarding the best type of exception to raise given the logic of a problem.
Advantages of using special return values: programmer convenience?
Disadvantages of using special return values: they require more interpretation and are non-viable without additional documentation.
Solution provided in book
None provided.
Execute this file
$ codedown ocaml < 2023-09-26-exceptions.md | grep . | ocaml -stdin