CSE130 LECTURE NOTES

March 14th, 2001

MISCELLANEOUS



EXCEPTION HANDLING

(cf. [Sebesta Chapter 13])

Let's think about the design of "escape" commands. In general a condition that makes an escape necessary is called an "exception". When an exception is triggered, and control escapes from inside a command, usually some recovery commands should be executed.

Here are some desirable features of exception handling:

  1. The single entry, single exit property should be preserved: that control can only flow into the command at one point, and can only flow out at one point.
    (history: ["GOTO considered harmful", Edsger Dijkstra, 1968]; follow up "GOTO considered harmful" considered harmful ...)

    Digression/Review: Control Statements

  2. It should be possible to raise exceptions and recover from them for any command, not just loops. There can be reasons to abort any command.
  3. When an exception is handled, it should be possible to execute some recovery commands as an alternative to the regular command which gave rise to the failure. For example if an exception is raised while reading an input value, it should be possible to recover by assigning a default value.
  4. It should be possible to provide exception handlers for many different types of exception.
  5. Providing exception handlers should be optional. If a handler is not provided, the exception should cause an escape from the enclosing block.
  6. Programmer-defined exceptions like "negative age" should be allowed in addition to system-defined exceptions like "division by zero".

Def.: An exception is an unusual event, erroneous or not, that is detectable either by hardware or software and that my require special processing, so-called exception handling. The code unit that does this is called an exception handler. The Ada language [Ada-FAQ] has a successful exception mechanism that meets all these requirements, as do other languages including Java. The mechanisms are very similar, so we'll just look at the Ada syntax. In general exception handlers occur at the end of blocks:

begin 
    block; 
exception 
    when e_1 => block_1; 
    when e_2 => block_2; 
    ... 
end;
If the exception e_i is raised while executing the block, then execution of the block is abandoned and block_i is executed instead.

If a recovery command is not provided for an exception, then the exception is propagated to the next enclosing block. For example:

begin 
    begin 
        block; 
    exception 
        when e1 => block_1; 
    end; 
exception 
    when e1 => block_1_1; 
    when e2 => block_2; 
    ... 
end;
In the code above, block_1_1 will never be executed, but block_2 may be.

A REALISTIC EXAMPLE OF EXCEPTION-HANDLING

Here is an example of an Ada procedure where three exceptions may be raised, and how those exceptions are handled. Note that negative_rainfall is a user-defined and user-raised exception, and that end_error is handled non-locally.
procedure main is 
    type 
        month is (jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec); 
        rainfall: array (month) of float; 

    negative_rainfall: exception; 

    procedure input_data is 
    begin 
        for amonth in month // implicit declaration of amonth!
            loop  
                    get(rainfall(amonth)); 
                    if rainfall(amonth) < 0.0 then raise negative_rainfall 
                exception 
                    when data_error => rainfall(amonth) := 0.0; 
                    when negative_rainfall => rainfall(amonth) := 0.0; 
    
                end; 
            end loop;
    end; 

begin 
    ... 
exception 
    when end_error => put("Insufficient data"); 
    when others => put("Unknown catastrophic error"); 
end
Some exceptions defined in Ada packages:

THE "EIFFEL" WAY TO USE EXCEPTIONS

It is bad programming style to use exceptions in place of regular testing for alternative cases, e.g. for end-of-file, or for leap years in software that processes dates.

The language Eiffel enforces a rule of good programming style, that the only ways to leave an exception-handler are

  1. to execute the failed operation again, or
  2. to propagate the exception.
With re-execution (called retry), after raising e1 and executing C1, C would be executed again. With propagation (called reraise), after raising e1 and executing C1, C1' would then be executed.

The use of exceptions in the Ada example above does not meet the Eiffel criteria.

In general, there are two main opinions on programming style in the presence of errors. The first attitude is that code should be written defensively, and as many errors as possible should be handled, even if they cannot be fixed completely.
The second attitude is that errors should be revealed as quickly as possible, so that they can be fixed properly.

For example, according to the first attitude, one should write

    
for (i = 0; i <= 10; i++) { ... } 
According to the second attitude one should write
 
for (i = 0; i != 10; i++) { ... } 
Suppose that the code inside the loop erroneously modifies i to have a value greater than 10. The first approach will hide this error, while the second will reveal it to the user, by causing an infinite loop.

EXCEPTION HANDLING IN JAVA

All Java exceptions are descendants of the class Throwable, including Error (e.g., out of heap space) and Exception. The latter has RuntimeException (e.g., ArrayIndexOutOfBoundsException and NullPointerException) and IOException as subclasses.

Here's the general form of these statements:

try {
    statement(s) } 
catch (exceptiontype) { statement(s) } 
...
catch (exceptiontype) { statement(s) } 
finally { statement(s) }
}

A throws clause is used by the compiler to ensure that all checked exceptions are handled appropriately (Error and RunTimeError exceptions are unchecked) by the calling method:

class C {
...
 void myMethd() throws IOException {   // handle elsewhere
  ...
  try { 
    ...
    if (...) throw new MyException(); 
    try { 
       ...
    }
    catch (ArrayIndexOutOfBoundsException) { ... }
    ... 
  } catch(MyException)
 }
}

Checked exceptions must be declared in the throws clause of the method throwing them (assuming, of course, they're not being caught within that same method). The calling method must take care of these exceptions by either catching or declaring them in its throws clause.

Continuations.

POTPOURRI

TYPE COMPATIBILITY
Haskell as a very elaborated strong type system based on type inference at compile-time (i.e., static typing). Not only are there polymorphic types (e.g., in [a], the type variable may be bound to any concrete type like Integer, or even to another polymorphic type such as BinTree a'):
data BinTree a = MTtree | Leaf a | Node (BinTree a) (BinTree a)
data MyList a  = Nil | Cons a (MyList a)

mybintree = Leaf 1
mylist    = Cons mybintree Nil

Main> :info mylist
mylist :: MyList (BinTree Integer)
Can we also have different tree types on the list?
...
mybintree2 = Leaf "abc"
mylist2    = Cons mybintree2 mylist

ERROR "types.hs" (line 14): Type error in application
*** Expression     : Cons mybintree2 mylist
*** Term           : mybintree2
*** Type           : BinTree [Char]
*** Does not match : BinTree Integer
 

In more traditional programming languages of the Algol family (e.g. Pascal) which have less powerful type systems, one distinguishes between

LANGUAGE DESIGN
OTHER PARADIGMS