(* 15-150, Spring 2024 *)
(* Michael Erdmann & Dilsun Kaynar *)
(* Code for Lecture 13: Exceptions *)
(************************************************************************)
(* Exceptions:
Declaring
Raising
Handling
*)
(************************************************************************)
val almostinfinity = 99999999999.9
(* Simple declaration: *)
exception Divide
(* Raising an exception: *)
(* divide : real * real -> real
REQUIRES: true
ENSURES: divide(r1,r2) ==> r1/r2 if r2 is not too close to 0.0
and raises exception Divide otherwise.
*)
fun divide(r1, r2) =
if Real.abs(r2) <= 0.0001 then raise Divide
else r1/r2
(* Declaration with an argument: *)
exception Rdivide of real
(* Raising an exception with an argument: *)
(* rdivide : real * real -> real
REQUIRES: true
Effects: rdivide(r1,r2) ==> r1/r2 if r2 is not too close to 0.0
and raises Rdivide(r1) otherwise.
*)
fun rdivide(r1, r2) =
if Real.abs(r2) <= 0.000001 then raise Rdivide(r1)
else r1/r2
(* Why is this useful?
Well, now we can pass back values in the exception.
This is particularly useful if we handle the exception:
*)
val result : real =
rdivide(3.14, 0.00000001)
handle Rdivide(r) => let
val _ = print("***ERROR: You tried to divide "^
(Real.toString r)^
" by a very small number!\n")
in
r*almostinfinity
end
(* Consider this function whose body is of the form
(e1 + e2) handle ...
*)
(* fun arith : real * real * real -> real *)
fun arith (x, y, z) =
(divide (x, y) + rdivide (y, z)) handle Rdivide r => r*almostinfinity
| Divide => almostinfinity
(* Then
arith (10.0, 2.0, 2.0) ==> 6.0
arith (10.0, 0.0, 2.0) ==> 99999999999.9
arith (10.0, 2.0, 0.0) ==> 200000000000.0
*)
(* Compare and contrast that with the next function, whose body is of the form
e1 + (e2 handle ...)
*)
(* fun arith' : real * real * real -> real *)
fun arith' (x, y, z) =
divide (x, y)
+
(rdivide (y, z) handle Rdivide r => r*almostinfinity
| Divide => almostinfinity)
(* Now
arith' (10.0, 2.0, 2.0) ==> 6.0 (* same as before *)
arith' (10.0, 0.0, 2.0) ==> no value - uncaught exception Divide raised
arith' (10.0, 2.0, 0.0) ==> 200000000005.0 (* different from before *)
*)
(* And one more example, from lecture: *)
exception Silly
fun f (x, 0) = raise Rdivide(x*x)
| f (x, n) = if n < 0 then raise Silly else x / real n
fun g (x, n) = f(x, n) handle Rdivide(v) => v
| Silly => 0.0
val 9 = round (g (3.0, 0))
val 0 = round (g (3.0, ~1))
val 3 = round (g (6.2, 2))
(************************************************************************)
(* *)
(* n-Queens, using Exceptions *)
(* *)
(************************************************************************)
(* threat : (int*int) -> (int*int) -> bool
REQUIRES: true
ENSURES: threat p q ==> true, if position p is threatened
by a queen at position q;
false, otherwise.
*)
fun threat (x, y) (x',y') =
(x=x') orelse (y=y') orelse (x+y = x'+y') orelse (x-y = x'-y')
(* conflict : (int*int) -> (int*int) list -> bool
REQUIRES: true
ENSURES: conflict p Q ==> true, if position p is threatened by any
queen in the list of positions Q;
false, otherwise.
*)
fun conflict pos = List.exists (threat pos)
exception Conflict
(* addqueen : int*int*(int*int) list -> (int*int) list
REQUIRES: Q is a list of conflict-free queen positions on an n x n board,
of the form [(i-1, _), (i-2, _), ... (1, _)],
with 1 <= i <= n (i=1 means Q is nil).
ENSURES: addqueen(i, n, Q) extends Q to a conflict-free placement
of n queens, if that is possible,
and raises exception Conflict otherwise.
Helper function try declared within the body of addqueen satisfies:
try : int -> (int*int) list
REQUIRES: as for addqueen.
ENSURES: try(j) extends Q to a conflict-free placement of n queens, with
the queen of column i in row j or higher, if that is possible,
and raises exception Conflict otherwise.
*)
fun addqueen(i, n, Q) =
let
fun try j =
(if conflict (i,j) Q then raise Conflict
else if i=n then (i,j)::Q
else addqueen(i+1, n, (i,j)::Q))
handle Conflict => (if j=n then raise Conflict
else try(j+1))
in
try 1
end
(* val queens : int -> (int*int) list
REQUIRES: n >= 1.
ENSURES: queens(n) computes a list of n conflict-free queen positions
on an n x n board, if that is possible,
and raises exception Conflict otherwise.
*)
fun queens(n) = addqueen(1, n, nil)
(* Some examples for which there are n queen placements: *)
val [(1,1)] = queens 1
val [(4,3), (3,1), (2, 4), (1,2)] = queens 4
val [(8,4),(7,2),(6,7),(5,3),(4,6),(3,8),(2,5),(1,1)] = queens 8
(* n=2 and n=3 do not permit conflict-free queen placements: *)
val false = (case queens 2 of _ => true) handle Conflict => false
val false = (case queens 3 of _ => true) handle Conflict => false
(* Here is a more stylish way to test for raised exceptions: *)
val NONE = SOME(queens 2) handle Conflict => NONE
val NONE = SOME(queens 3) handle Conflict => NONE
(* We could also have defined queens to return an option directly: *)
(* val queens : int -> (int*int) list option *)
fun queens(n) = SOME(addqueen(1, n, nil)) handle Conflict => NONE
val SOME[(1,1)] = queens 1
val SOME[(4,3), (3,1), (2, 4), (1,2)] = queens 4
val SOME[(8,4),(7,2),(6,7),(5,3),(4,6),(3,8),(2,5),(1,1)] = queens 8
val NONE = queens 2
val NONE = queens 2
(************************************************************************
In the previous implementation, each call to addqueen creates its
own version of try. Each version of try has the variables i,n,Q
bound within the environment of its closure. The variables i,n,Q
do not change as one is trying different positions (i,j) within a
given column i, so one does not need to pass them as arguments to
function try. Indeed, defining a local function like try avoids
rebinding those unchanging variables over and over as j varies.
However, deciding to define try is really a matter of taste/design.
One could simply add j as another argument to addqueen,
and perhaps implement n-Queens as follows:
************************************************************************)
(* addqueen : (int*int)*int*(int*int) list -> (int*int) list *)
fun addqueen((i,j), n, Q) =
(if conflict (i,j) Q then raise Conflict
else if i=n then (i,j)::Q
else addqueen((i+1,1), n, (i,j)::Q))
handle Conflict => (if j=n then raise Conflict
else addqueen((i,j+1), n, Q))
(* val queens : int -> (int*int) list option *)
fun queens(n) = SOME(addqueen((1,1), n, nil)) handle Conflict => NONE
(* Some examples for which there are n queen placements: *)
val SOME[(1,1)] = queens 1
val SOME[(4,3), (3,1), (2, 4), (1,2)] = queens 4
val SOME[(8,4),(7,2),(6,7),(5,3),(4,6),(3,8),(2,5),(1,1)] = queens 8
val NONE = queens 2
val NONE = queens 2
(************************************************************************
For comparison here is an implementation of n-Queens with
addqueen and try returning an option rather than potentially
raising Conflict (with try as a locally-defined helper function):
************************************************************************)
(* addqueen : int*int*(int*int) list -> (int*int) list option
REQUIRES: Q is a list of conflict-free queen positions on an n x n board,
of the form [(i-1, _), (i-2, _), ... (1, _)],
with 1 <= i <= n (i=1 means Q is nil).
ENSURES: addqueen(i, n, Q) evaluates to SOME(Q'), where Q' extends Q
to a conflict-free placement of n queens, if that is possible,
and evaluates to NONE otherwise.
Helper function try declared within the body of addqueen now satisfies:
try : int -> (int*int) list option
REQUIRES: as for addqueen.
ENSURES: try(j) evaluates to SOME(Q') where Q' extends Q to a
conflict-free placement of n queens, with the queen of
column i in row j or higher, if that is possible,
and evaluates to NONE otherwise.
*)
fun addqueen(i, n, Q) =
let
fun try j=
(case (if conflict (i,j) Q then NONE
else if i=n then SOME((i,j)::Q)
else addqueen(i+1, n, (i,j)::Q))
of NONE => if (j=n) then NONE else try(j+1)
| result => result)
in
try 1
end
(* queens : int -> (int*int) list option
REQUIRES: n >= 1.
ENSURES: queens(n) evaluates to SOME of a list of n conflict-free
queen positions on an n x n board, if that is possible,
and evaluates to NONE otherwise.
*)
fun queens(n) = addqueen(1, n, nil)
val SOME([(1,1)]) = queens 1
val NONE = queens 2
val NONE = queens 3
val SOME([(4,3), (3,1), (2, 4), (1,2)]) = queens 4
val SOME([(8,4),(7,2),(6,7),(5,3),(4,6),(3,8),(2,5),(1,1)]) = queens 8
(************************************************************************
For further comparison here is an implementation of n-Queens
using success and failure continuations.
The failure continuation takes unit as an argument and, when called,
effectively implements the backtracking. Observe that each new
call to try creates a new failure continuation; the lexical
scoping rules regarding closures ensure that calling this continuation
is the same as backtracking, i.e., continuing the search from the
place the continuation was defined.
Notice that the success continuation sc never changes and that
the code calls sc upon success using a tail call. There
are no pending computations to unravel; exit upon success is quick.
************************************************************************)
(* addqueen : int*int*(int*int) list -> ((int*int) list -> 'a)
-> (unit -> 'a) -> 'a)
REQUIRES: Q is a list of conflict-free queen positions on an n x n board,
of the form [(i-1, _), (i-2, _), ... (1, _)],
with 1 <= i <= n (i=1 means Q is nil).
ENSURES: addqueen (i, n, Q) sc fc is equivalent to one of the following:
(a) sc(Q'), where Q' extends Q to a conflict-free placement
of n queens, if that is possible;
(b) fc(), otherwise.
Helper function try declared within the body of addqueen now satisfies:
try : int -> 'a
REQUIRES: as for addqueen.
ENSURES: try(j) is equivalent to one of the following:
(a) sc(Q'), where Q' extends Q to a conflict-free placement
of n queens, with the queen of column i in row j or
higher, if that is possible;
(b) fc(), otherwise.
*)
fun addqueen (i, n, Q) sc fc =
let
fun try j =
let
fun fcnew () = if j=n then fc() else try(j+1)
in
if (conflict (i,j) Q) then fcnew()
else if i=n then sc((i,j)::Q)
else addqueen(i+1, n, (i,j)::Q) sc fcnew
end
in
try 1
end
(* queens : int -> (int*int) list option
REQUIRES: n >= 1.
ENSURES: queens(n) evaluates to SOME of a list of n conflict-free
queen positions on an n x n board, if that is possible,
and evaluates to NONE otherwise.
*)
fun queens(n) = addqueen (1, n, nil) SOME (fn () => NONE)
val SOME([(1,1)]) = queens 1
val NONE = queens 2
val NONE = queens 3
val SOME([(4,3), (3,1), (2, 4), (1,2)]) = queens 4
val SOME([(8,4),(7,2),(6,7),(5,3),(4,6),(3,8),(2,5),(1,1)]) = queens 8
(***************************************************************************
Here is an implementation of n-Queens that uses just failure continuations.
Consequently, we have less flexibility over the final return type.
***************************************************************************)
(* addqueen :
int*int*(int*int) list * (unit -> (int*int) list) -> (int*int) list
Spec is the same as before except that case (a) now returns Q' directly.
*)
fun addqueen(i, n, Q, fc) =
let
fun try j =
let
fun fcnew () = if j=n then fc() else try(j+1)
in
if (conflict (i,j) Q) then fcnew()
else if i=n then (i,j)::Q
else addqueen(i+1, n, (i,j)::Q, fcnew)
end
in
try 1
end
(* queens : int -> (int*int) list
REQUIRES: n >= 1.
ENSURES: queens(n) evaluates to list of n conflict-free queen
positions on an n x n board, if that is possible,
and evaluates to nil otherwise.
*)
fun queens(n) = addqueen(1, n, nil, fn () => nil)
val [(1,1)] = queens 1
val nil = queens 2
val nil = queens 3
val [(4,3), (3,1), (2, 4), (1,2)] = queens 4
val [(8,4),(7,2),(6,7),(5,3),(4,6),(3,8),(2,5),(1,1)] = queens 8
(************************************************************************
Here is an alternative implementation based on failure continuations,
in which we have rearranged the code slightly, to make it easier to
write the failure continuations in-line. On the other hand, now
the nesting of if-then-elses becomes somewhat long.
************************************************************************)
(* addqueen :
int*int*(int*int) list * (unit -> (int*int) list) -> (int*int) list
*)
fun addqueen(i, n, Q, fc) =
let
fun try j =
if j=n+1 then fc()
else if (conflict (i,j) Q) then try(j+1)
else if i=n then (i,j)::Q
else addqueen(i+1, n, (i,j)::Q, fn () => try(j+1))
in
try 1
end
(* queens : int -> (int*int) list
REQUIRES: n >= 1.
ENSURES: queens(n) evaluates to a list of n conflict-free queen
positions on an n x n board if that is possible,
and evaluates to nil otherwise.
*)
fun queens(n) = addqueen(1, n, nil, fn () => nil)
val [(1,1)] = queens 1
val nil = queens 2
val nil = queens 3
val [(4,3), (3,1), (2, 4), (1,2)] = queens 4
val [(8,4),(7,2),(6,7),(5,3),(4,6),(3,8),(2,5),(1,1)] = queens 8
(************************************************************************
Here is yet another version mimicking the version that has both success
and failure continuations, but now with those two continuations merged
into a single continuation that takes an option as an argument.
(This version however does not have the nice quick exit upon success
that the version with two continuations had previously. Instead,
the code must unravel nested function calls that case on options.)
************************************************************************)
(* addqueen :
int*int*(int*int) list -> ((int*int) list option -> 'a) -> 'a
*)
fun addqueen (i, n, Q) k =
let
fun try j =
let
fun k' NONE = if j=n then k(NONE) else try(j+1)
| k' result = k result
in
if (conflict (i,j) Q) then k'(NONE)
else if i=n then k'(SOME((i,j)::Q))
else addqueen (i+1, n, (i,j)::Q) k'
end
in
try 1
end
(* queens : int -> (int*int) list option
REQUIRES: n >= 1.
ENSURES: queens(n) evaluates to SOME of a list of n conflict-free
queen positions on an n x n board, if that is possible,
and evaluates to NONE otherwise.
*)
fun queens(n) = addqueen (1, n, []) (fn (x : (int*int) list option) => x)
val SOME([(1,1)]) = queens 1
val NONE = queens 2
val NONE = queens 3
val SOME([(4,3), (3,1), (2, 4), (1,2)]) = queens 4
val SOME([(8,4),(7,2),(6,7),(5,3),(4,6),(3,8),(2,5),(1,1)]) = queens 8
(************************************************************************)