------------------------

Harvey Mudd College
Computer Science 131
Programming Languages
Spring Semester 2000

Lecture 05

------------------------

------------------------

One problem that programmers often face is what a function should do in aberant cases where there is no obvious sensible answer. This is generally caused by one of two situations.

------------------------

The first aberant situation is where the input is reasonable, but there is no answer, for example, when you are looking up a name in a database, but there is no entry for the name:

fun lookup _ nil = 0
  | lookup searchName ((name,age)::t) = 
       if (name = searchName) 
         then age
         else lookup searchName t

fun lookup' name db = 
       case (lookup name db) of
          0   => (print "That name (" ; print name ; 
                  print ") was not in the database!")
        | age => (print name ; print "is " ; print (Int.toString age) ; 
                  print "years old.")

But this is a hack. The choice of value for "undefined" will depend heavily on the application, and sometimes there may be no good choice.

SML provides a solution to this problem in the form of option types. In general, for a type 'a, a value of type 'a option is either the value NONE or the compound value SOME a, where a is a value of type 'a.

So, we can rewrite the last function as:

fun lookup _ nil = NONE
  | lookup searchName ((name,age)::t) = 
      if (name = searchName) 
         then SOME age
         else lookup searchName t

fun lookup' name db = 
       case (lookup name db) of
          NONE     => (print "That name (" ; print name ; 
                             print ") was not in the database!")
        | SOME age => (print name ; print "is " ; print (Int.toString age) ; 
                             print "years old.")
You can see from the type reported for lookup that this is a much more general solution. In fact, it would probably be more appropriate to write the first function as:

fun lookup _ nil = NONE
  | lookup searchKey ((key,value)::t) = 
      if (key = searchKey) 
         then SOME value
         else lookup searchKey t
------------------------

In the other situation, the output is undefined because the input was somehow inappropriate. This is in general an error, and in ML it is handled by using the exception mechanism.

------------------------

Suppose the hd list destructor were not built into the language. It is simple to define ourselves as:

fun head (h::t) = h;
What happens when it is compiled?

 

What happens when we call it with an empty list as argument?

 

In order to have a custom error message occur for this situation we need to first define the error, and then use it appropriately:

exception Head;

fun head nil = raise Head
  | head (h::t) = h;

Below we will discuss how to trap exceptions so that they don't cause the program to break back to the main SML prompt.

In many cases it will be useful to have the exception carry with it some information to be used in the exception handler. For example, we might want to raise an exception when the factorial function is called with a negative number. For debugging purposes it would be useful if the error included the problem value. We can do this as follows:

exception Factorial of int;

fun fact 0 = 1
  | fact n = if (n < 0)
               then raise (Factorial n)
               else n * fact (n-1);
Unfortunately, the current SML-NJ implementation does not echo back the parameters of untrapped exceptions. But they will be available to you when you actually trap the error yourself.

By the way, what is unfortunate about the way I just defined factorial (aside from the fact that it is not tail-recursive). How would you fix it to be more efficient?

 

 

 

 

------------------------

The basic paradigm for catching exceptions is that you wrap whatever expression might result in an exception being raised in a handler.

For instance, suppose we defined the lookup function for a string dictionary to raise an exception if it couldn't find the key it was looking for, as in:

exception NotFound of string;
 
fun lookup key nil = raise (NotFound key)
  | lookup key ((key1,def)::tail) = 
       if (key = key1)
         then def
         else lookup key tail;

Now, we might write the following function that given a list of strings and a dictionary, prints out the definition of each string:

fun printDefs dict nil = ()
  | printDefs dict (h::t) = 
       (print ("\"" ^ h ^ "\": " ^ (lookup h dict)) ;
        print "\n" ; 
        printDefs dict t);

Now, consider the call:

val dict = [("a","alpha"),("b","beta"),("d","delta")];

printDefs dict ["a","b","c","d"];

I have two options as regards trapping this exception. First, I can trap it at the outer level by wrapping the call to printDefs in a handler:

fun printDefsHandle dict keys = (printDefs dict keys)
                                    handle (NotFound key) => 
                                       print ("The key: \"" ^ key ^ 
                                       "\" was not found. Process aborted.\n");

printDefsHandle dict ["a","b","c","d"];

Now, I can also catch the exception lower down, inside the loop. This will enable me to continue with the loop where I left off, but it requires rewriting printDefs:

fun printDefs dict nil = ()
  | printDefs dict (h::t) = 
       (print ("\"" ^ h ^ "\": " 
               ^ ((lookup h dict) handle (NotFound key) => "undefined")
               ^ "\n"); 
        printDefs dict t);
Now, the original call is well behaved:
printDefs [("a","alpha"),("b","beta"),("d","delta")] 
           ["a","b","c","d"];

------------------------

The general form of a handler allows several different exceptions to be trapped. The syntax is:

   (expression) 
      handle (exn_1 arg) => 
               (handler expression_1)
           | (exn_2 arg) => 
               (handler expression_2)
           | ...
           | _ => 
               (general handler)
The patterns on the left of the arrow are arbitrary pattern-matching expressions and can manipulate the exception and the data it carries in all the usual ways. In this case, the last case lets you trap any exceptions you might not have thought of.

------------------------
This page copyright ©2000 by Joshua S. Hodas. It was built on a Macintosh. Last rebuilt on Monday, January 31, 2000.
http://www.cs.hmc.edu/~hodas/courses/cs131/lectures/lecture05s.html