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

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. In languages with weaker type systems the typical solution is to pick some value out of the range of the return type and designate it as the undefined value; for example, returning 0 as the age of a person not in the database, as in:

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, for example, 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. We have already encountered exceptions in the case of programs with non-exhaustive match errors.

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

All SML error messages occur as the result of some exception being raised by the system because something has gone wrong. The exception mechanism is similar to that of Java, but is somewhat less cumbersome. In particular, you do not have to declare what exceptions you raise, and are not responsible for wrapping every block that might raise an exception with a handler.

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

fun head (h::t) = h;
But what happens when we call it with an empty list as argument. How does this compare with the behavior of the built-in destructor? In order to have a custom error message occur for this situation (which would be much more informative to the programmer) 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. This will be important when we are writing our interpreters, since we want those to behave like self-contained systems.

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"];

This guards against the uncaught exception, but the process still aborts and returns at the point the exception is caught since there is no way to re-enter the loop.

Notice, by the way, that the entire expression made up of the call to printDefs together with the handler wrapped around it is itself an expression. Since sometimes (when the exception is never raised) the underlying expression will be the thing producing the value of the overall expression, and sometimes (when the exception is raised) the handler will be producing the value returned for the overall expresssion, the handler an the underlying expression must return the same type. Here, since they both return unit there is no problem. Sometimes it is tricky to get this issue right. It is not suprising that exception handlers most commonly are placed in functions that just print things and return unit.

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 dict ["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/lecture05.html