This page describes a project for COM 3360: Adaptive Object-Oriented Software Development, Fall 1996. The main project page for the class is here.
The project lives on in an implementation project. Read all about it.
December 12, 1996
A second issue centers around how exceptions can be used adaptively, that is, how exception throwing and handling can be decoupled from other computation.
A third issue is this: does AP give us the opportunity to use exceptions in new and exciting ways, or does it perhaps make it easier to use exceptions in traditional ways?
This document introduces exceptions and their use in Java in Section 2. A discussion about the nonlocal nature of exceptions can then be found in Section 3, where the potential conflict between exceptions and the Law of Demeter is also pointed out. Section 4 then describes the purpose of this project in some detail. A simple design for how exceptions can be used with adaptive programs is presented in Section 5, and Section 6 extends this design to one that allows exception behavior as context objects. Section 7 considers some new ways of using exceptions that are made possible by the high abstraction level of Demeter programs. Section 8 discusses the possible implementation of most of the design. Section 9 contains a description of patterns of exception use that I have found by studying existing software in several languages. Finally, Section 10 relates some miscellaneous, peripheral observations that I have made while working on this project and that I have been asked to commit to a disk block.
In Java, the syntax for handling an exception is the following:
try { ... code that might throw an exception ... } catch (IOException e) { ... code that inspects e ... } ... more catch clauses ... finally { ... general cleanup code ... }where the catch clauses and the finally clause are optional (although at least one of them must be present). The code in the try block is executed, and if it finishes without any exception being thrown, the code in the finally block is executed. If an exception is thrown during the execution of the try block, however, then that exception is reified as an object of a class derived from the system class Exception, and the first catch clause that matches the type of the exception class is entered. After the catch clause has finished executing, the finally clause is executed.
In addition to exceptions being thrown by system methods (e.g. as a result of I/O errors), programs can themselves throw exceptions using the throw statement:
throw expressionwhere the expression must evaluate to an object of an exception class.
The language definition requires that a method be declared with a list of the exception classes that may escape from the method; that is, those exceptions that are thrown by the method itself or by methods called by the method, and which are not handled by the method. For example,
public int my_method( int n ) throws IOException, IEEEException { ... }The method exception list becomes part of the interface to the method (and thereby part of the class of which the method is a part) and in fact, it is possible for a Java compiler to check statically whether all exceptions escaping from a method are in fact declared in the exception list.
A method that overrides or hides another method must have an exception list that is a (not necessarily proper) subset of the list in the method that is being overridden or hidden.
The definition of Java exceptions may be found in Gosling [1]. The mechanisms for exception handing in C++ [2] and Modula-3 [3] are similar to the mechanism used by Java. A somewhat dated survey of exception handling mechanisms (covering PL/1, Ada, MESA, and CLU) may be found in the book by Horowitz [4], which also contains a discussion about how exception handling differs from normal returns and the kinds of situations in which exception handling is natural, as well as a summary of the design space of exception handling mechanisms.
One can see from the preceding description that changes to the interface of a method can result in large-scale changes to the source code of a program; for example, if an exception is added to the method's exception list, it will have to be added to the exception lists of all the method's callers, transitively, up to points where the new exception can be handled. Exception declarations are therefore non-local, and programs that use exceptions may be harder to change than those that don't. (In fact, Ellis and Stroustrup [2] report that the throw declarations on functions are optional in C++ for precisely this reason.)
It seems to me that such propagation of static information throughout the program is a flagrant violation of the Law of Demeter.
Some techniques have evolved to deal with this problem. In many cases, a method interface is designed carefully so that the method will throw a generic exception that carries further information about the exception. If the method implementation is changed so that it calls a method that signals an exception not previously handled by the caller, then the caller will handle that exception locally and map it onto the already-existing generic exception class. The generic exception class is already declared as part of the interface of every caller of the original method that does not handle it, so no global changes are needed. The only changes needed are to those methods that actually handle the exception: they need to be extended to handle the new subcase. The following example (in Modula-3, I have yet to find a single Java program that actually uses exceptions in an interesting way) illustrates the point:
TRY frame.fullPathname := FS.GetAbsolutePathname (filename) EXCEPT | OSError.E (list) => RAISE FormsVBT.Error (RdUtils.FailureText (list)) END;
Even with the use of this and other techniques for using exceptions, the problem persists: programs that are to be changeable and undergo evolution in the way AP assumes that they will, may not be as adaptable when they are using exceptions as when they are not, since the addition or subtraction of an exception may require global program changes.
I see it as an important goal that if we are to annotate the Java programs that are created by Demeter/Java with information about which exceptions pass through which program points, then this information should be as precise as we can reasonably make it, and the design must reflect this.
A proposal for such a design is presented in Section 5.
Section 6 discusses these ideas in some detail and also proposes a design for exception contexts.
Section 7 contains a discussion of these issues.
What I mean by "traditional" exception usage is the following. Exceptions are thrown in procedures and caught in other procedures; in particular, exception-throwing code and exception-handling code is woven into computational code. Exceptions are represented as (user-defined) objects and carry data. Programming techniques of the type exemplified by Barrow's rules will be used when writing exception-handling code.
There are three aspects to the current design: before and after method annotations, exception handlers in and annotations on traversals, and a methodology for dealing with verbatim methods. Sections 5.1 through 5.3 discuss these aspects. Section 5.4 discusses edge visitors. Section 5.5. shows how new exception classes can be declared in the Class Dictionary. Section 5.6 summarizes the extensions, and section 5.7 lists some design ideas that were considered but that are not part of the proposal.
Team { traversal collectBullpen(BullpenCollector b) { bypassing -> *,disabled,* to Pitcher; } } BullpenCollector { before Pitcher (@ pitchers = pitchers.append(host)); @) throws ListTooLongException; }If pitchers.append() may throw ListTooLongException, then the caller of that method must either catch it or declare it as being thrown; in this example, it is declared as being thrown.
Team { traversal collectBullpen(BullpenCollector b) { bypassing -> *,disabled,* to Pitcher; } protect Team (ListTooLongException e) (@ throw new TeamTooLarge( "collectBullpen" + b.collectorName() ); @) throws TeamTooLarge; } BullpenCollector { before Pitcher (@ pitchers = pitchers.append(host)); @) throws ListTooLongException; }Here, the protect clause is an attribute on the traversal; the class name used could be any class name along the path from Team to Pitcher. The meaning is that if the ListTooLongException is thrown along any path of the traversal below Team, then that exception must be caught in the Team class, and the handler code to be executed is as is given.
There can be multiple protect clauses on a traversal.
A traversal may throw more exceptions than are thrown by the visitors' before and after methods, as is the case in this example, and that's what the throws clause on the traversal is for: it is a list of all the exceptions that may be thrown by the exception handler. (Hence, one throws goes with one protect, although it is optional. Possibly, the throws clause should go before the body, not after, to be even more Java-like.)
Notice the use of the visitor "b" in the code block of the protect clause. All traversal contexts are also parameters to the exception handler code and it is an error if the name to which the exception object is bound during handling conflicts with any context name for the traversal.
The keyword protect was chosen because the protect clause behaves differently from the Java catch clause; catch was otherwise the obvious candidate. I'm not dogmatic about this.
TeamTooLargeException = <info> MyInfo *extends* Exception .where the additional part objects are optional, of course. (Demeter/Java already supports this functionality.)
Wrapper : Before | After *common* <hosts> HostSpec JavaCode [ ThrowsList ]. ThrowsList =The meaning of these constructs has already been specified."throws" Commalist(ClassName) ";" Traversal = "traversal" TraversalName TraversalArgs "{" *l + PathDirective ";" - *l "}" [ <protect> ProtectClauses ]. ProtectClauses = List(ProtectClause) . ProtectClause = "protect" CatchBody [ Finally ]. CatchBody = ClassGlobSpec "(" ClassName ExceptionName ")" JavaCode [ ThrowsList ]. Finally = "finally" CatchBody . ExceptionName = <name> DemIdent.
Arguably, this is a more general functionality than is currently provided by the protect mechanism on the traversal, and I think it's worthwhile to consider an around mechanism in the future.
For example,
BullpenCollector { around Team // parameters: (SomeThunkType thunk, Team host) (@ try { thunk.apply(); } catch (ListTooLongException e) { throw new TeamTooLarge( ... ); } @) throws TeamTooLarge; }At this time, it is unclear how the Thunk class would be declared and how the exception annotations on the apply() method would be generated, although I believe it can be done. There can be many Thunk types, however, since the program has "no" need of manipulating them, and that may save us.
The around methods are more general than this.
The basic idea is that exception behavior (throwing and catching) can be packaged as a context object and passed as a parameter to a traversal.
The above assumption is under no circumstances uncontroversial. In the given example, I suspect that the exception throwing is in fact part of the core behavior, and I further suspect that any time an exception is caused not by program errors or by external causes, it will be part of the core behavior. Therefore, a separate exception context may not be a natural or comprehensible programming technique. Furthermore, it seems like an unnatural or pointless solution for when exceptions originate from external sources.
The reason why I will still discuss (entertain? :-) the idea is that it does lead to an interesting decoupling of exception behavior (both throwing and catching) from other computational behavior; this may lead to a higher degree of reuse and a lower amount of duplicated code but is in any case a novel view of exceptions, as far as I know.
Company { traversal computeSalary( SummingVisitor s ) { to Salary; } protect (TotalException e) (@ s.total = -INFINITY; @) } SummingVisitor { before Salary (@ total += host.get_salary(); if (total < 0) throw new TotalException(); @) throws TotalException; }Notice how the exception code and the computational code are woven together. It might be useful to parameterize the traversal with an additional context object that carries the exception code. The following has been suggested (although with a different syntax) by Karl Lieberherr:
Company { traversal computeSalary( SummingVisitor s, ExceptionContext e ) { to Salary; } } SummingVisitor { before Salary (@ total += host.get_salary(); @) } ExceptionContext { thrower Salary (@ if (s.total < 0) throw new TotalException(); @) throws TotalException; catcher Company catch (TotalException e) (@ s.total = -INFINITY; @) }(The preceding code assumes that the SummingVisitor s is stored in the ExceptionContext in the usual way.) The following extensions to the Demeter/Java grammar define the syntax of the full construct. The production Wrapper has been extended w.r.t. the definition in section 5.6 in order to accomodate the catcher keyword, which differs from the other wrappers.
Wrapper : SimpleWrapper | CatchWrapper. SimpleWrapper : Before | After | Thrower *common* <hosts> HostSpec JavaCode [ ThrowsList ]. CatchWrapper = Catcher <hosts> HostSpec "catch" CatchBody. Thrower = "thrower". Catcher = "catcher".
The semantics of exception contexts are the following. The thrower method is run before traversals into part objects of the host, after the before methods of context objects preceding the exception context in the traversal's parameter list have been run, and before the before methods of context objects following it in that parameter list. If the thrower does not throw an exception, it is for all intents and purposes a regular before method. The catcher method is run if any traversal to part objects from an instance of the class named by catcher ("Company" in the above example) throws an exception of the type named ("TotalException") or of a subclass of that type.
Let's analyze the advantages of the division of core behavior and exception behavior. While the exception context is tied to the SummingVisitor (a tie that could be loosened somewhat with proper use of subclassing), the traversal and the SummingVisitor are both seemingly independent of the ExceptionContext.
If exception contexts can be subclassed, then many different exception behaviors can be passed to the traversal. The traversal specification can be reused for each case, and the SummingVisitor can be used in many different contexts. Subclassing exception contexts may not be straightforward; the issues surrounding such subclassing are discussed in section 6.3.
Even if subclassing is not an alternative, there are other techniques that can be used to achieve the same result, to a point. For some traversals, one can construct exception contexts that are the union of other contexts (for different traversals) and use the union context for several traversals. In other cases, one can use delegation: there is one exception context that is used to determine at which classes throwing and catching should be done, but all the logic could be confined to methods of a delegate object installed in the context object. This is not nearly as nice as subclassing, but it works.
In the current version of Demeter/Java, there are restrictions on how visitors can be subclassed. For example, the following (pseudo-code) is legal but does not do what you expect:
Visitor { } Visitor1 extends Visitor { before Class1 (@ ... @) } Visitor2 extends Visitor { before Class2 (@ ... @) }If a traversal is declared as taking a Visitor object, both Visitor1 and Visitor2 can be passed to the traversal, but the before methods will not be called. Instead, Visitor must be defined in the following manner:
Visitor { before Class1 (@ @) // empty before Class2 (@ @) // empty }(It is my understanding that this restriction will be removed in the future.) In the case of visitors, the workaround probably works fine. For exception contexts, however, we must be more careful. For reasons outlined above, Demeter cannot know (at compile-time) which exact subtype of an exception context is used with a particular traversal. Therefore, exception throwers and catchers should also behave like virtual methods. Consider:
EC { catcher Class1 catch (Exception1 e) (@ @) // empty } EC1 extends EC { catcher Class1 catch (Exception1 e) (@ ... @) }This form of subclassing (where the handlers are virtual) should be sufficient to implement exception context hierarchies where the various pieces have very different behavior, in the same sense that the visitors in the previous example applied to different classes. Consider:
EC { catcher Class1a catch (Exception1 e) (@ throw e; @) thrower Class1b (@ @) throws Exception1; catcher Class2a catch (Exception2 e) (@ throw e; @) thrower Class2b (@ @) throws Exception2; } EC1 extends EC { catcher Class1a catch (Exception1 e) (@ bomb(); @) thrower Class1b (@ throw new Exception1; @) throws Exception1; } EC2 extends EC { catcher Class2a catch (Exception2 e) (@ bomb(); @) thrower Class2b (@ throw new Exception2; @) throws Exception2; }Notice the use of the statement throw e in the body of the catchers in the class EC. This is necessary for orderly propagation of the exception in the absence of an actual handler for the given exception; for example, if EC2 is used and Exception1 is thrown by the Visitor, then that exception must not be dropped on the floor by the empty handler in the base class.
(Exception1 can indeed be thrown by the Visitor (or one of its subclasses!) without declaring this fact in the exception context -- the use of exception contexts does not preclude the use of traditional exception programming behavior.)
A suggestion was made (by Mitch Wand) that in addition to adding information to an exception object in some procedures as an exception propagates back through the call graph, one might also want to add information to the object as the exception propagates back through some edges. This would require adding exception handlers to the edges of the call graph, which as far as I know is a not previously implemented idea. It's not hard to simulate edge handlers with handlers in the vertices (procedures), however, but I suspect they're not usually thought of as edge handlers.
Using the design from section 5, we could envision something like the following:
Team { traversal collectBullpen(BullpenCollector b) { bypassing -> *,disabled,* to Pitcher; } protect -> *,active,* (ListTooLongException e) (@ throw new TeamTooLarge( "collectBullpen" + b.collectorName() ); @) throws TeamTooLarge; }The meaning here is that if a ListTooLongException propagates back through an "active" edge, then it should be caught and the new exception should be thrown. As usual, actual class names can be used in place of the "*" as the source and sink of the edge.
To make this truly workable, it would be necessary to come up with a scheme for allowing the handler access to the source and sink objects, in the cases where their types are determinable. I leave this as future work.
As far as exception handlers on classes are concerned, it is easy to see that the protect clause tells the Demeter/Java system exactly which classes handlers need to be set up in. Each class will have a traversal method for every traversal. It is therefore possible to set up the exception handler by emitting the traversal method with the otherwise-generated method body wrapped in a try-catch block.
As for annotations on the methods, they can be computed using standard flow-graph operations, by considering the object graph to be a flow graph and the classes to be procedures. Sets of exceptions are analogous to variable definitions (assignments), and exception handlers are the last "uses" of the exception. The exception ranges are then analogous to live ranges; an exception is live at a node if it is live at any callees of the node and not handled in the node. An edge wrapper can be handled by splitting the edge in two and adding a node to join the two parts.
Given classes C1 and C2 and edge E from C1 to C2 and an exception handler H on that E, the traversal will call from some method C1.n to some method C2.m to perform the traversal rooted at C2. However, every other edge from some Ci to C2 will also go along edge E to C2.m, but we don't want to put handlers on those edges, so we can't put the handler in C2.m.
The handler H can be put in C1.n, around the call to C2.m; this is probably the simplest. The handler will want to have access to both objects, in some circumstances at least, and both are available from C1. (In fact, one can argue that H has to go in C1.n: if the exception is a NullPointerException that is raised because C2 is not there, then the programmer will still want to catch it on that edge.)
... try { ... try { ... } catch (SomeException e) { ... } ... } catch (SomeOtherException e) { ... }
Here are some speculations about the reasons for this programming style. First, some of the programs are relatively old, dating from the early '90s; they may predate many innovations in style rules for OO programming. Second, a number of the programmers are old Modula-2+ hands; my impression of Modula-2+ is that it is not a particularly object-oriented language. Third, the class hierarchies were shallow and the code was complex, so the programs were emphasizing computation over data representation and hence perhaps somewhat non-OO. Fourth, and in my opinion most important, however, is that anyone who has tried to write a visitor-pattern based OO program by hand for a nontrivial class hierarchy will know that it is simply not a natural way of writing a program.
Complete use of visitors makes it a real pain both to write the program and to understand the resulting flow of control -- like unrestricted use of higher-order functions, it is simply not a good way of writing programs that are to be read by humans. The visitor pattern introduces levels of indirection in the program and these impede construction and understanding. The use of tools like Demeter certainly makes it easier to write visitor-based programs, and in my very limited experience it also makes it possible to read and understand those programs. Such tools are therefore in my estimation not only aids in construcing some kinds of OO programs, but fundamentally necessary.
Assume that it is important that the traversal is reasonably fast. Then the problem becomes to create at run-time a program that performs the traversal efficiently. There are several ways of doing this:
The method I will be describing in this note has the flavor of an interpreter interpreting traversal instructions, but at the same time behaves very much like a specialized traversal procedure.
Consider having an interpreter for all the program's object graphs; such a procedure or set of procedures can be constructed by the Demeter/Java system, as I have already argued, and it can be directed by instructions as to which edges to traverse and which edges to leave alone. The trick is to find the right instruction set, the right encoding, and a fast interpretation mechanism.
The instruction set consisting of the two instructions YES and NO is pretty small, and it's the one I'll be using.
The encoding is also simple: any object reference means YES, and a null object reference means NO.
The trick is in the interpreter. The interpreter is not the object graph itself, as suggested above, but a meta-graph or prototypical graph: a graph that models the class hierarchy specified in the Class Dictionary. Consider this grammar:
A = <b> B <c> C. B = <d> D <e> E. C = <f> F. D = <b> B <g> G. E = . F = <h> H. G = .The grammar can be modeled by an object graph that has objects that correspond to the classes, with part-objects that correspond to the edges. There will be seven objects in the graph, one for each of A..G. The graph may be circular, and for the above grammar it will.
A traversal is a specification of a subgraph of this graph. For example, the traversal from A to G goes through B and D, and includes a back-edge from D to B. More importantly, the <c> edge in the A object is nil, as is the edge <e>. There are A, B, D, and G objects in the subgraph.
Now let the subgraph be the interpreter, and let each object X in the subgraph have a method rttrav that takes as its input an object of the class for which X is the proto-object; e.g., protoA.rttrav() takes as input an object of class A. The body of protoA.rttrav() will then look like this:
void rttrav( A a, Visitor v ) { if (b != null) b.rttrav( a.get_b(), v ); if (c != null) c.rttrav( a.get_c(), v ); }The interpreter overhead is thus a load and compare-with-null of an instance variable, per edge in the actual object graph. If the instruction is YES, that is, non-null, then the edge is there, and if the instruction is NO, that is, null, then the edge is not there.
Now, how do we implement the before and after methods? One possibility is to make the Demeter/Java system add two methods to each visitor object, called rtbefore and rtafter, which take a magic integer and an object and switch on the integer, invoking the right method (if any):
void rttrav( A a, Visitor v ) { if (a.before) v.rtbefore( NAME_A, this ); if (b != NULL) b.rttrav( a.get_b() ); if (c != NULL) c.rttrav( a.get_c() ); if (a.after) v.rtafter( "A", this ); }The rtbefore method might look like this:
void rtbefore( int name, Object ) { switch (name) { NAME_A : this.before_A( Object ); break; NAME_B : this.before_B( Object ); break; } }When the traversal is constructed, the before and after methods in the proto-objects are set to true or false as appropriate. (Arbitrary visitors cannot be constructed at run-time.)
Some issues have been tossed around but are still resolved.