CSE130 LECTURE NOTES


November 17, 1999
 
 

ADMINISTRATION

Reminder: the lecture that would have been on Wednesday next week, before Thanksgiving, has been moved to Friday this week.  As an incentive for you to come on Friday, there will be a very interesting special handout.

The third project is due next Monday, which is when the fourth project will be distributed.
 
 

EXCEPTIONS IN ADA

On Monday we saw four PL design criteria for exception-handling.  The Ada language has a successful exception mechanism that meets all these requirements, as do ML and 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
    C;
exception
    when e1 => C1;
    when e2 => C2;
    ...
end;
If the exception ei is raised while executing the command C, then C is abandoned and Ci 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
        C;
    exception
        when e1 => C1;
    end;
exception
    when e1 => C1';
    when e2 => C2;
    ...
end;
In the code above, C1' will never be executed, but C2 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
            loop
                begin
                    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

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.
     
     

    WHY TYPES ARE IMPORTANT

    Now we know what the range of possibilities is for types in programming languages. In all programming languages, every value has a certain type, and the compiler and/or the runtime system knows what the type is.

    Why is it so important to give each value a type?  There are several answers to this question.

    (1) The implementation of operations like addition is different depending on the types of the operands.

    (2) If the programmer gives operands of the wrong type, this is an error that should be reported to her or him.

    (3) The type of a variable is important documentation about the software. For example, it is very useful to know whether <2.5,3.7> is of type rectangular or polar, where

    type rectangular = record x, y: real end;
    type polar = record r, theta: real end;

     

    STATIC VERSUS DYNAMIC TYPE-CHECKING

    Definition: Type-checking is the process of checking that for each operation, the types of its operands are appropriate, i.e. that the operation is legal for these types.

    Static type-checking means type-checking done by the compiler, before executing the program.

    Dynamic type-checking means type-checking done while executing the program. The compiler must generate extra code to do this checking.

    The descriptor strong type-checking is sometimes used to mean static type-checking.  Alternatively, it is used to mean complete type-checking, i.e. guaranteeing that any mismatch is caught between the signature of an operation and the types of its actual arguments.
     
     

    WHY COMPILE-TIME TYPE-CHECKING IS VALUABLE

    (1) If the compiler can know for sure the types of operands, then it can generate code for the right implementation of (e.g.) addition with no space overhead of generating code for alternative operations, and with no run-time overhead of testing and choosing one of the implementations.

    (2) The compiler can use complex type-checking algorithms just once, with no run-time speed penalty.

    (3) Errors can be reported at compile-time instead of run-time.
    (3)(a) Experts on software engineering and management say that the later an error is caught, the more it costs to fix the error.
    (3)(b) Some run-time type conflicts may not be caught at all during testing, because they only happen for un-tested input values.

    This last reason (3) is the most important.
     
     

    WHY RUN-TIME TYPE-CHECKING IS VALUABLE

    (1) Human input is often of unpredictable type. Languages with run-time typing can deal with this more easily.

    (2) At run-time, type-checking can know the actual values of variables, not just their initial value or declared type.

    In practice all languages combine run-time and compile-time type-checking. Verifying that index variables are within range can often only be done at run-time.

    Question to think about: when can it be done at compile-time?

    (3) Less verbosity.  Example: the awk language.
     
     

    THE ISSUE OF TYPE EQUIVALENCE

    When doing type-checking, one big issue arises: how to decide whether one type is a legal substitute for another. Let's look at two cases.

    Case 1: Think of the rectangular and polar types above, and consider a procedure that displays a point on the screen:

    procedure plot (p: rectangular);
    begin
        (* code *)
    end;
    Should it be legal to call plot(q) where q is a variable of type polar? Intuitively, the plot procedure will probably do the wrong thing with q.

    Case 2: Consider a procedure that just writes out a tuple for debugging purposes:

    procedure debugwrite (v: record x,y: real end);
    begin
        writeln(x,y);
    end;
    Intuitively, this procedure should be a legal operation to perform on variables of type rectangular and also of type polar.
     



    Copyright (c) by Charles Elkan, 1999.