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

Harvey Mudd College
Computer Science 131
Programming Languages
Spring Semester 1999

Lecture 06 (2/8/99)

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

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

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 from a string dictionaries to raise an exception if it couldn't find the key it was looking for, as in:

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

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

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

Now, consider the call:

print_defs [("a","alpha"),("b","beta"),("d","delta")] 
           ["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 print_defs in a handler:

(print_defs [("a","alpha"),("b","beta"),("d","delta")] 
            ["a","b","c","d"])
   handle (Not_Found key) => 
             print ("The key: \"" ^ key ^ 
                     "\" was not found. Process aborted.\n") ;

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 print_defs 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 re-enter the loop where I left off, but it requires rewriting print_defs:

fun print_defs dict nil = ()
  | print_defs dict (h::t) = 
       (print (("\"" ^ h ^ "\": " ^ (lookup h dict)) 
                handle (Not_Found key) => 
                    ("The key: \"" ^ key ^ 
                     "\" was not found. Process continuing."));
        print "\n" ; 
        print_defs dict t);
Now, the original call is well behaved:
print_defs [("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 last case lets you trap any exceptions you might not have thought of.
------------------------

Over the next few weeks we will do almost all our programming in an almost purely functional style. We'll work almost exclusively without a reall "assignment" or variable update. While most of you are probably still skeptical, I think at least a few of you are already beginning to see that in a rich setting like ML, recursion is powerful tool that nearly does away with the need for assignment.

But, while you can write just about anything in this style, the creators of SML realized that there were some times when programming in the traditional imperative way, with updates, is a lot simpler. So, now I want to talk about the question of how to get variable names whose values we can change when we really really really feel we need them.

The way you do this in SML is by the ref type, which really amounts to an explicit pointer. We've already had pointers in our code implicitely. Whenever you use a constructor it really builds a cell with pointers to the component parts of the structures. But using those pointers was entirely hidden, and we could imagine, for example, that the left sub-tree cell of a tree node really did contain the whole left sub-tree rather than a pointer to it. In contrast, refs are basically explicit pointers.

To construct a cell whose conents you want to be able to change and give it a name, you use the ref constructor, which takes as its argument the initial value in the cell. For example:

val x = ref 3;
Now, to update the value of the cell, you use the update operator, which basically dereferences the pointer and changes the value in the cell:
x := 4;
Notice that an update operation has the return value unit. This is true of all side-effects in SML and is done to discourage you from using it as part of a computation and thus confuse it with the functional fragment of the language. To explicitly dereference the pointer and get the value at the end of it, you use the bang (!) operator. Compare the results of:
x;
!x;

Now, to see how this affects things, recall the example from a few lectures back and compare it to the results that occur when refs are used:

val j = 5;
val j1 = ref 5;

fun add_j k = j + k; fun add_j1 k = !j1 + k;

add_j 3; add_j1 3;

val j = 7; j1 := 7;

add_j 3; add_j1 3;

There is an important difference between these pointers and the kind used in C. Since a pointer is only created by the ref constructor, and since there is no explicit pointer disposal function, there is no such thing as an unitialized or dangling pointer! This is part of what it means for ML to be a safe language.

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

A common use of refs is to provide the functionality of static variables in C. That is, to store some local mutable value between calls to a function. For example, we could implement a function that counts how many times it is called by:

local 
   val count = ref 0
in 
   fun foo () = (
                 count := !count + 1 ;
                 !count
                )
end;

Note, the series of expressions separated by semicolons is a sequence. The semicolon in this context is the infix sequencing operator (though it's not defined as an operator, it is special syntax). It evaluates its arguments in order and returns the value of its second argument. This is typically used when side-effects are desired, as with refs or printing.

We can extend this program to allow the starting value to come in as a parameter by building the function on the fly and returning it:

fun new_counter start =
   let
      val count = ref start
   in 
      fn () => (
                count := !count + 1 ;
                !count
               )
   end;

This will allow us to create many counters with separate storage cells.

Similarly, we could create a linear congruential pseudo-random number generator as follows:

local 
   val multiplier = 25173
   val increment = 13849
   val modulus  = 32768
in
   fun new_random seed = 
      let
         val current = ref seed
      in 
         fn range => (
                      current := ((!current * multiplier) + 
                                  increment) mod modulus ;
                      !current mod range
                     )
      end
end;

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

We can take this idea a step further and even get a notion of object with mutable state. The following code will implement a counter object that can be incremented and decremented:

exception illegal_action;
 
fun mk_counter () = 
      let
         val counter = ref 0
      in
         fn "Val" => !counter
          | "Inc" => (
                    counter := !counter + 1; 
                    !counter
                   )
          | "Dec" => (
                    counter := !counter - 1; 
                    !counter
                   )
          | "Reset" => (
                    counter := 0; 
                    !counter
                   )
          | _ => raise illegal_action
      end;

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

One potential problem with refs is that if you are not careful, they open a potential hole in the ML type system.

Consider the declaration:

val x = ref (fn x => x);
What is the type of x?

Then what do we make of the following expression:

(x := (fn x => x + 1);
!x true)
Each part is correctly typed, yet they cannot be combined into a single typable expression.

For this reason, SML restricts refs to being built essentially on monomorphic values. That is, in general you must know their precise type at the time you create them. The type must be known before the ref is created.

The restriction extends up to structures and functions built from refs. So, the function:

fun f x = ref [x];
is legal, but cannot be applied to nil.

This restriction gives rise to the general ``value type restriction'' that we have discussed earlier, and which has plagued us occasionally. For a full description see


http://cm.bell-labs.com/cm/cs/what/smlnj/doc/Conversion/types.html

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

This page copyright ©1999 by Joshua S. Hodas. It was built on a Macintosh. Last rebuilt on Tuesday, February 9, 1999 at 2:09:10 PM.
http://cs.hmc.edu/~hodas/courses/cs131/lectures/lecture06.html