November 17, 1999
The third project is due next Monday, which is when the fourth project
will be distributed.
In general exception handlers occur at the end of blocks:
If the exception ei is raised while executing the command C, then C is abandoned and Ci is executed instead.begin
C;
exception
when e1 => C1;
when e2 => C2;
...
end;
If a recovery command is not provided for an exception, then the exception is propagated to the next enclosing block. For example:
In the code above, C1' will never be executed, but C2 may be.begin
begin
C;
exception
when e1 => C1;
end;
exception
when e1 => C1';
when e2 => C2;
...
end;
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 language Eiffel enforces a rule of good programming style, that the only ways to leave an exception-handler are
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.(1) to execute the failed operation again, or (2) to propagate the exception.
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 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 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.
(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.
(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.
Case 1: Think of the rectangular and polar types above, and consider a procedure that displays a point on the screen:
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.procedure plot (p: rectangular);
begin
(* code *)
end;
Case 2: Consider a procedure that just writes out a tuple for debugging purposes:
Intuitively, this procedure should be a legal operation to perform on variables of type rectangular and also of type polar.procedure debugwrite (v: record x,y: real end);
begin
writeln(x,y);
end;