↑ Writing ↑

GEONius.com
28-May-2003
 E-mail 

TPOCC and Object-Oriented Programming

A Tutorial/Proposal

Having been written way back in 1991,
this memo is obviously not the last word on
Eiffel, C++, or object-oriented software!

The Problem
Eiffel
Objects
Inheritance
Multiple Inheritance
Polymorphism and Dynamic Binding
Genericity
Assertions
Exceptions
Why Eiffel?
Why Not Eiffel?
Bibliography


Although the display subsystem is the most outwardly visible (and vocal) component of a TPOCC-based system, it is only the tip of the iceberg relative to the full functionality of a satellite control center and to the TPOCC software itself. Consequently, it seems that disproportionate amounts of time and effort are devoted to researching and evaluating advanced display capabilities, while the remaining TPOCC subsystems are relegated to the use of 20-year old software technology.

The T in TPOCC stands for transportable. TPOCC software is intended to be transportable, in the sense that it should be reuseable and easily adapted to new missions. The TPOCC software is programmed in C, a quasi-assembly language that is not conducive to reuseability and maintainability. This memo presents an overview of object-oriented programming (OOP) and proposes an investigation of Eiffel, an OOP language that has the potential of significantly increasing the reuseability of TPOCC software without adversely affecting its performance.

 

The Problem

The mission-independent nature of the generic TPOCC software is achieved through a heavy reliance on the database management system (DBMS). Unfortunately, doing so places limits on the performance and flexibility of the TPOCC software. For example, imagine decommutating the high-frequency, attitude data found in the LANDSAT-4/5 telemetry stream (TM sensor). There are 16 sets of 12-bit, X/Y/Z attitude data per minor frame of telemetry. Storing a location map of this data in the TPOCC DBMS would require 16*3*2 (= 96) entries, with a subcommutation depth of 1. (The factor of 2 occurs because each datum is 8+4 = 12 bits wide.)

96 entries will not bring the DBMS to its knees, but they do add 96*128 (= 12,288) processing points to the decommutation program's data structures. A processing point is the table-driven extraction of bits from a specified telemetry word in a specified minor frame. Using 12,288 processing points to extract just the attitude data from a 4-second major frame is wasteful of memory and CPU cycles.

A simpler, more efficient approach is to bypass the table altogether and to hard-code the attitude data extraction in the program. Once per minor frame, a simple loop is executed that extracts the 48 attitude values from the minor frame. Now, only 128 "processing points" are required to extract the attitude data from a major frame. Admittedly, a hard-coded "processing point" is more involved than a table-driven processing point, but it can be heavily optimized (e.g., coded in assembly language) to make it very fast. However much the table-based approach is optimized, it still must face the fact that it gets executed at least 12,288 times per major frame.

The generic TPOCC decommutation program contains a "hook" that allows a new mission to incorporate mission-specific processing functions into the final, compiled and linked program. After each minor frame is decommutated, the "hook" routine is called to apply any mission-specific processing to the minor frame. While this scheme can accommodate the attitude data extraction method described in the previous paragraph, imagine a scenario where the mission-specific processing must be performed before the minor frame is decommutated; e.g., to reverse pairs of bytes. Peppering the generic decommutation code with hooks is one means of building flexibility into the program, but where do you start and stop? Another alternative is to make mission-specific modifications to a copy of the generic software. Doing so is undesirable, however, because these modifications must be reviewed and made anew whenever a new TPOCC release arrives - a mission pressed for time might be reluctant to upgrade.

Object-oriented programming languages overcome the problem of flexibility by the manner in which they reuse code. Eiffel offers this flexibility, while approaching the performance of traditional languages such as C.

 

Eiffel

Eiffel is an object-oriented programming language and environment developed by Bertrand Meyer and Interactive Software Engineering. Eiffel has the safeguards of a strongly-typed language like Ada, the readability of a language like Pascal, the confidence-instilling simplicity of FORTRAN (as opposed to the worrisome complexity of Ada), and the performance of C (well, almost!). Eiffel is its own language, but it is easily integrated with software written in other languages (e.g., the existing TPOCC software).

Unlike pure object-oriented languages such as Smalltalk and hybrid languages such as C++, Eiffel was designed from the ground up to support four key concepts of object-oriented programming: inheritance, polymorphism, dynamic binding, and genericity. All of these contribute to the reuseability of Eiffel code; they will be described further in subsequent paragraphs.

 

Objects

Object-oriented design models a system in terms of the objects which compose the system. An object can correspond to a physical entity or an abstract concept. Graphical objects in display systems are frequently used as examples, but that's almost cheating, since the objects are so obvious. Applying object-oriented design techniques to a less trivial application (telemetry decommutation, for instance) is more of a challenge, but the potential benefits motivate the attempt.

An object is characterized by its state and its behavior, where behavior effects transitions to new states. In OOP languages, an object is represented by attributes which define the object's state and functions which query or modify the object's state. A human object that does nothing but eat and sleep all day might have a single attribute, a boolean variable eating; two state change functions, Eat() and Sleep(); and two state query functions, IsEating() and IsSleeping().

An object's class defines the type of the object; instantiating the class produces an instance of the object. For example, human objects are of class human; Laurel and Hardy are instances of that class. In conventional programming terms, the class of an object is its data type; an instance is just a variable of the given data type. Giving birth to Laurel and Hardy is a painless exercise in Eiffel (see Listing 1).

A function is invoked on an object by simply calling the function associated with the object. For example, Laurel.IsSleeping returns true if Laurel is sleeping and false if he is awake. Other OOP languages, such as Smalltalk, use a message passing paradigm, as well as different terminology: attributes are known as instance variables and functions are called methods. A method is invoked by sending a message, specifying the method and any required arguments, to the object of interest. With this approach, a Sleep message sent to Laurel would put Laurel to sleep.

 

Inheritance

Inheritance is a mechanism by which an object class inherits attributes and/or functions from a parent class. With multiple inheritance, an object class may inherit features from more than one parent. A simple example of multiple inheritance is a child who inherits his father's good looks and his mother's brains. (It is interesting to note that, in biological inheritance, characteristics are inherited from instances of objects; in object-oriented inheritance, characteristics are inherited from types of objects.) As you might guess, inheritance is a major factor contributing to the reuseability of object-oriented software.

Classes, subclasses, and superclasses - terms used by some OOP languages to describe an inheritance hierarchy - don't accurately convey the import of inheritance. Inheritance can either restrict or extend the functionality of a parent class. A novice ham radio operator, for instance, is a special type of ham radio operator with restricted privileges. A ham with an "extra class" license, on the other hand, is a ham radio operator with expanded privileges and capabilities. The ham class hierarchy looks as follows (note that this and subsequent diagrams show the logical relationships between types of objects and not physical links between instances of objects):

                                 General Class
                                    License
                                   /       \
                  Specialization  /         \  Extension
                                 /           \
                          Novice Class    Extra Class
                            License         License

In object-oriented programming, specialization is achieved by redefining or hiding attributes and functions inherited from a parent class; extension is achieved both by redefinition and by adding new attributes and functions to those inherited from the parent class(es).

Any discussion of inheritance brings up the notion of abstract classes. Known as deferred classes in Eiffel and meta-classes in X Windows, abstract classes serve as place-holders in an object hierarchy; they are never instantiated. A deferred class is frequently used to define the basic features of an object type without specifying the details of their implementation. For example, a deferred class, MNF, may be declared that defines the attributes and functions of a generic minor frame (see Listing 2). Create is a standard Eiffel function that instantiates an object; in this case, Create is redefined to allocate a buffer large enough to hold a minor frame. Table-driven decommutation, as done in the current TPOCC software, is performed by an internal routine, TableDrivenExtraction.

A mission-specific class, missionMNF, would inherit the features of the MNF class, but redefine them or add to them in order to capture the required mission-specific functionality (see Listing 3). In this example, missionMNF inherits the attributes and functions of its superclass, MNF; MNF's Create() and Decommutate() functions are renamed so that they can be referenced in missionMNF's redefined functions. Two new constants are defined and function definitions deferred in MNF are supplied in missionMNF.

Note how the Decommutate() function is redefined to first reverse the order of the bytes in the minor frame and then to call the parent class's decommutation function (which performs the table-driven extraction of data). No "hooks" are required to add mission-specific processing; just redefine the relevant functions and insert the mission-specific actions where you need them. LANDSAT's Decommutate(), for instance, might call the generic decommutation function before calling an external assembly language routine to extract the voluminous attitude data.

Although the diagram of a class hierarchy looks a lot like a structure chart, object classes are not usually developed in a top-down fashion. Frequently, a programmer writes specific object classes first, and then, noting the similarities between the classes, generalizes the common attributes and functions into higher-level, abstract classes. Thus, development proceeds bottom-up, from more specific classes to more general classes.

 

Multiple Inheritance

Multiple inheritance is drawing increasing attention in the object-oriented programming world. The earlier example of a child inheriting characteristics from both parents belies the difficulty of applying multiple inheritance in object-oriented programming. Single inheritance is typically used to represent a logical is-a relationship between a parent class and its children. Multiple inheritance can be used for two purposes: (i) to represent some logical relationship between the parent classes and a child, or (ii) to share code among classes.

The former use seems obvious, but there is a dearth of practical examples in the OOPS literature, a fact that has been noted in the same literature. My meager contribution is a radio:

                          Transmitter    Receiver
                                \         /
                           is-a  \       /  is-a
                                  \     /
                               Transceiver

A transceiver has all the attributes and functions of a transmitter and all the attributes and functions of a receiver. Note that a transceiver object is not simply a grouping of a transmitter object and a receiver object, it is an indivisible object in its own right.

Borrowing code from two or more parent classes is the most common use of multiple inheritance in practice. Called a "marriage of convenience" by Bertrand Meyer, this form of multiple inheritance has no qualms about stealing attributes and functions from logically-unrelated classes. Dr. Meyer gives the example of a noble, but financially destitute, English family that marries their daughter off to an unrefined, American oil baron of questionable lineage; the family estates are preserved at the expense of watering down the family tree.

Multiple inheritance for "convenience" has distinct advantages in object-oriented programming. First, it is a prime example of reusing code. Second, it can produce more efficient code, by reducing the level of indirection in an object's implementation. The latter reason borders on the "let's all use assembly language" philosopy, but it is worth keeping in mind. The code reuse properties of multiple inheritance, on the other hand, could be usefully applied to the TPOCC display program.

The TPOCC display subsystem is an X Windows-based program that manages the operator's interface to a TPOCC-based control center. Objects (e.g., numeric values, stripcharts, etc.) on a display page are animated by telemetry data points received from the TPOCC data server, which samples the data as it is decommutated. The X and OSF/Motif toolkits follow a particular methodology that simulates single inheritance using the standard C language. Objects (known in X as widgets) are represented by record structures, which contain attributes and pointers to functions. Inheritance is achieved by including the parent class's object structure in the child class's object structure.

An animated display object is known as a real-time widget; i.e., it is animated by real-time data received from the data server. To initiate a stream of data for an object, certain information must be passed to the data server identifying what data should be sent. These items of information are essentially attributes of a real-time display object and include the host name of the computer where the data is found, the process name and mnemonic of the desired telemetry point, the rate at which the data should be sampled, the data's data type, an array index if required, etc.

The display of simple text on the screen is done using Motif label objects/widgets. Examples of textual displays include animated ASCII text, numeric telemetry values, telemetry data quality indicators, discrete state names, and GMT. To implement these objects, our display developer created a TPOCC real-time label class, RTLabel, that inherits the base attributes and functions of the Motif label class, XmLabel, and adds in the attributes and functions required for a real-time display object. The textual display objects, in turn, inherit the features of the TPOCC real-time label class and add in their own individual attributes and functions. The class hierarchy looks as follows:

                                  XmLabel
                                     |
                                  RTLabel
                 /         /         |          \         \
             Ascii     Numeric   QualityTag     Range     Time

More complex text objects, such as the spacecraft command buffer page (CPAGE) or the events window, are displayed using Motif's scrolled window object/widget. As with the label class, a real-time scrolling text class, RTScrollT, was created as an intermediary display class:

                             XmScrolledWindow
                                    |
                                RTScrollT
                                /       \
                            Cpage     EventW

(Actually, there's another class in between XmScrolledWindow and RTScrollT, but it can be ignored for our purposes.) The intermediate RT classes were introduced solely to add the real-time functionality to the Motif objects/widgets. Due to the limitations of single inheritance, there is a great deal of code and data declarations duplicated between the RTLabel and RTScrollT classes. Code reuse this is, but of the sledgehammer variety!

Factoring out the features of a generic real-time display object produces a multiple inheritance hierarchy that looks as follows:

                     XmLabel     RTObject    XmScrolledWindow
                            \     /     \    /
                             Ascii      Cpage
                            Numeric     EventW
                              ...

(A "partial" class such as RTObject, which is purposefully designed as an abstract class for use in a multiple inheritance hierarchy, is sometimes called a mixin.) By eliminating the duplication of code and data, multiple inheritance offers more readable and reliable software, increased programmer productivity, and reduced maintenance costs.

 

Polymorphism and Dynamic Binding

In conventional programming languages, variables can be assigned values; e.g., X = 23. Strongly-typed languages such as FORTRAN restrict the types of values that can be assigned to a particular variable. For example, a floating point number can be stored in a floating point variable, but not in an integer variable. Weakly-typed languages, usually interpreted, relax these restrictions and allow you to assign any type of value to a given variable. In TSTOL, for instance, a variable may, at different points in its lifetime, be an integer variable (X = 23), a floating point variable (X = 0.314E+01), or a string variable (X = "Hello!").

Object-oriented programming languages also support variables, but the value assigned to a variable is actually an object, complete with all of its attributes and functions. Untyped languages such as Smalltalk allow you to assign objects of any class to a particular variable. This is known as polymorphism; i.e., the variable can take on "many forms".

In typed languages like Eiffel and C++, polymorphism must conform to the class hierarchy of the objects in a program. In other words, a variable of class type C can be assigned any object belonging to class C or to any subclasses derived from C (via inheritance or multiple inheritance). For example, consider the following class hierarchy:

                                    MNF
                                     |
                                LANDSAT_MNF
                                   /   \
                             MSS_MNF   TM_MNF

(The LANDSAT spacecraft has separate streams of telemetry data for its two sensors: the Multi-Spectral Scanner and the Thematic Mapper.) A variable declared of type MNF can be assigned a generic MNF object, a LANDSAT_MNF object, an MSS_MNF object (no high-frequency attitude data), or a TM_MNF object (with HF attitude data). It would be illegal to assign HUMANs or Transceivers to a MNF variable.

An object carries along its attributes and its functions, so the code fragment in Listing 4 simultaneously processes MSS and TM telemetry data. GetNextMNF() is a function that returns the next available minor frame from either of the telemetry data streams, MSS or TM. Cycle() simply loops, reading and decommutating each minor frame received.

How is sensor-specific decommutation performed? Although, in Listing 4, thisMNF is declared as a generic MNF variable, the function call, "thisMNF.Decommutate", is not simply a reference to the generic MNF class's decommutation function. Instead, the function call is bound at run-time to the decommutation function defined in the dynamically-assigned minor frame object; i.e., the decommutation functions defined in the MSS_MNF and TM_MNF classes. Dynamic binding is the mechanism that ensures that a function call invokes the appropriate function for the target object.

Dynamic binding is not as trivial as it may seem. For example, the TM_MNF class redefines the generic decommutation function so that attitude data extraction is performed in addition to the normal, table-driven decommutation. MSS minor frames, however, require no special processing, so the MSS_MNF class simply inherits the generic MNF decommutation function. For MSS minor frames, then, the statement, "thisMNF.Decommutate" really would execute the generic MNF Decommutate().

By writing software in terms of abstract class objects and letting polymorphism and dynamic binding resolve the details of specific class instances, an object-oriented programmer can produce flexible, reuseable code on a scale that is difficult to achieve with non-object-oriented languages. A frequently-used example of this flexibility is a loop that redisplays a list of objects. In a traditional language like C, case statements are needed to handle the polymorphic nature of display objects:

    object = displayList ;
    while (object != NULL) {
        switch (typeOf (object)) {    /* Redisplay object. */
        case POINT:  DisplayPoint (object) ;  break ;
        case LINE:   DisplayLine (object) ;  break ;
        case CIRCLE: DisplayCircle (object) ;  break ;
        }
        object = object->next ;        /* Get next object. */
    }

If a new type of graphical object (e.g., rectangle) is added to the display system, the code above must be modified by adding a new case statement for the new object type. OOP languages like Eiffel, on the other hand, eliminate case statements and code modification altogether:

    object: GRAPHICAL_OBJECT ;

    from object := displayList.First until object = void loop
        object.Display ;               -- Redisplay object.
        object := displayList.Next ;   -- Get next object.
    end ;

Dynamic binding ensures that the appropriate display function is called for each object type (e.g., point, line, circle, etc.). Adding a new display type such as a rectangle is a simple matter of defining a new object class, RECTANGLE, that is a descendant of the abstract GRAPHICAL_OBJECT class. No changes need to be made to the basic display loop. This example illustrates an excellent observation made by Palsberg and Schwartzbach: inheritance permits the reuse of definitions, while polymorphism allows the reuse of applications.

The X Windows Toolkit, written in C, avoids case statements for its object/widget types by implementing a semi-interpreted form of dynamic binding. (The following is a simplified description of X Toolkit callback procedures.) Basically, a resource name (e.g., "display") is associated with a field (e.g., a pointer to the display() function) in an object/widget structure. A program can then ask the Toolkit to call a function, identified by its resource name, for an arbitrary widget. The Toolkit looks up the resource name, finds the function pointer in the target widget's structure, and calls the function, passing it the address of the target widget. This technique is clever, but slow.

Although adding a case statement for a new object is not an onerous task, even for a display programmer, what if a change has a more far-reaching, system-wide impact. For example, the TPOCC data services subsystem is responsible for supplying application programs with synchronous and sampled real-time data. The data server receives synchronous ("as-it-is-generated") data from one or more synchronous data sources; sampled data is periodically read from shared memory. Both types of data are served out to application clients that request the data. The following diagram shows a typical TPOCC configuration:

        Telemetry                Data      ,-----> [6] Display [7]
          Decom  [2] -----> [3] Server [5] +-----> [6] Reports [8]
           [1]                    [4]      `-----> [6] TSTOL   [9]
            |                      ^
            |                      |
            `-----------------> Shared
                                Memory

The telemetry decommutation program is a synchronous data source (and also the original source of sampled data); the display, reports, and TSTOL processes are application clients.

The data server handles many different types of data: integers, floating point numbers, character strings, and several types of telemetry current value table (CVT) entries. Each CVT is a structure containing a decommutation status word, a raw telemetry value, and an engineering units-converted value. The raw values themselves are of different data types: signed and unsigned integers, single- and double-precision reals, etc.

How do the TPOCC programs cope with these different data types (26 at last count)? With case statements, of course. The bracketed numbers in the diagram above identify places in the TPOCC software where data type-specific processing is performed:

[1] Decommutation of different types of telemetry data.
[2] Encoding of different types of telemetry data for transfer across the network.
[3] Decoding of different data types received over the network.
[4] Fetching different data types from shared memory.
[5] Encoding different data types for transfer across the network.
[6] Decoding of different data types received over the network.
[7] Displaying different data types.
[8] Printing different data types.
[9] Manipulating different data types; e.g., performing arithmetic operations.

Pity the poor programmer who has to add a new telemetry CVT type. The effects of the new addition, initially felt in the decommutation program, would propagate throughout the system: data server, display, reports, and TSTOL. Common library routines and table-driven processing alleviate the problem somewhat, but making the necessary modifications to all the affected subsystems is still a thankless job, tedious and prone to error. I should know - I had to add 6 new CVT types recently!

Would an object-oriented approach help here? Using an OOP language would simplify considerably the decommutation ([1] above), display ([7]), printing ([8]), and manipulation ([9]) of data objects, as well as the encoding of these objects for output on the network ([2] and [5]). It is not clear, however, how software that receives data could decode objects of different types without performing a case selection based on the object's data type.

An interesting problem in object-oriented programming has to do with multiple polymorphism. The discussion above dealt with single polymorphism, where the target object is polymorphic. Calling Display() for a graphical object will invoke the display function bound to that object; e.g., display a line, display a circle, etc. Suppose, however, the display function expects (as an argument or a global variable) a polymorphic object representing the output device, e.g., a video monitor, a laser printer, etc. Two levels of polymorphism are now present, one for the object to be displayed and a second for the device on which the object will be displayed:

                      Point  ---v--->  Video Monitor
                       Line  ---+--->  Laser Printer
                     Circle  ---'

Where does the code to display a graphical object on a video monitor reside? Device-specific code could be embedded in the graphical object, with case statements selecting the different display types. Adding a new output device would then require changes to each of the implemented graphics types. A better solution is to add graphics-specific display functions to the device objects. If its Display() function were invoked, a graphical object would then pass itself (designated by the reserved word Current in Eiffel) to the device object's Display<Graphic>() function:

    class CIRCLE_OBJECT export
        Display
    inherit
        GRAPHICAL_OBJECT redefine Display ;
    feature
                   -- Display circle on specified device.
        Display (port: DISPLAY_DEVICE) is
        do
            port.DisplayCircle (Current) ;
        end ;
    end

The abstract DISPLAY_DEVICE class contains display functions for each of the available graphics types. The display functions for primitive graphical objects (e.g., points and lines) are device-dependent and, therefore, their definitions are deferred in the abstract class. Subclasses for specific devices (e.g., video monitor or laser printer) must provide actual drawing routines. Higher-level, composite graphics can frequently be plotted using the graphics primitives, so their display functions can be spelled out in the abstract class (and redefined, if need be, in a subclass). For example, the DisplayRectangle() function constructs a rectangle by drawing the 4 sides.

Adding a new output device has no effect on the existing graphical objects and device objects. The new device class inherits its features from the abstract DISPLAY_DEVICE class and defines any deferred functions. Adding a new graphical object requires only that a new DisplayGraphic() function be added to the abstract DISPLAY_DEVICE class and to each of the device subclasses. If the new graphic can be drawn using existing primitives, the device subclasses can simply inherit the new display function from the abstract class.

 

Genericity

TSTOL (TPOCC Systems Test and Operations Language) is the command language interpreter for TPOCC-based systems. Among its many capabilities, TSTOL can read and execute commands stored in TSTOL procedure files. TSTOL procedures are similar to FORTRAN subroutines and, like them, can be nested; e.g., procedure A can call procedure B, B can call C, and so on.

TSTOL keeps track of procedure nesting using a number of stacks; two of them will be described here. The Procedure File Table is a stack of procedure file records, one record for each procedure that has been called in the current nesting. Each record contains information about a procedure: the name of the file containing the procedure, the number of lines in the file, the current line number, and the actual text read from the file. When a procedure is called, a new file record is created and pushed on the stack, the contents of the procedure file are input and stored in the record, and the current line number is set to 1. As commands are executed, the current line number is incremented. When a procedure returns, its file record is popped from the stack and discarded; execution then continues in the procedure that is now exposed on the stack.

TSTOL is a block-structured language that supports global variables and procedure-local variables. TSTOL's scoping rules prevent a procedure (e.g., C) from accessing variables local to an ancestral procedure (e.g., A or B) and vice-versa. To manage local variables in nested procedures, TSTOL's symbol table is also structured as a stack. When a procedure is called, a new block for the procedure's local variables is created and pushed on the Symbol Table Stack. If a variable reference is made in a procedure, the top block on the stack is searched to see if the reference is to a local variable; if not, the block reserved for global variables is then searched. When a procedure returns, its local variable block is popped from the stack, effectively deleting the procedure's local variables, and the local variable block of the parent procedure is uncovered.

The procedure file stack and the symbol table stack are both stacks, with the same operations defined for each: push, pop, etc. The only difference between the two stacks is the type of the object pushed or popped: procedure file records on the procedure file stack and local variable blocks on the symbol table stack. After writing the software implementing one stack, a productive programmer would look for ways to reuse the code on the other stack. C offers a limited form of code reuse, called (by me) "cut-and-paste" reuse, in which the degree of code reuseability depends on how powerful your text editor is.

Eiffel and some non-object-oriented languages (especially Ada) offer a more elegant mechanism for reusing code in situations like that described above. Genericity is a means of parameterizing a unit of software, with supplied parameters substituted in the definition of the unit when it is compiled. Generic subprograms and packages are a much-touted feature of Ada. Eiffel allows you to define generic object classes (not to be confused with deferred classes like the generic MNF). For example, a generic STACK class could be defined with a single parameter, T, specifying the type of the objects on the stack.

TSTOL's procedure file and symbol tables can now be implemented as stacks, without having to rewrite the stack operations for each table because of the different element types. To illustrate, the design of the procedure file table is presented in Listing 7. A procedure file record contains the text read from the procedure file (a private attribute) and keeps track of the current line number in the file (a public attribute). Function GetLine() returns the text of the current line in the file and increments the line number.

The class for the procedure file table declares a private variable, procedureStack, as a stack of procedure file records. When a new procedure is started, function PushFile() is called to read the procedure file in and push its record on the stack. Two functions, GetLine() and GoToLine(), are then used to step through the current procedure (on the top of the stack). When a procedure exits, PopFile() is called to pop its record from the stack.

The symbol table is organized in a similar fashion, but is too lengthy to show here. Three classes are required. A SYMBOL object is simply a name/value pair. A SYMBOL_BLOCK object is a list of symbols, all of which belong to the same block. Finally, the SYMBOL_TABLE class contains a single SYMBOL_BLOCK for global variables and a stack of SYMBOL_BLOCKs (declared as STACK[SYMBOL_BLOCK]) for local variables. When a new procedure is started, an empty block is pushed onto the local variable stack. Variables declared within the procedure are then added to the top block on the stack. Variable references are resolved by first searching for the symbol in the current local variable block (on top of the stack) and then, if the symbol was not found, the global variable block is searched. When the procedure exits, its local variable block is popped from the stack.

The generic STACK class is an example of unconstrained genericity; i.e., there are no restrictions on the type of object for which the stack is instantiated. You can define a stack of integers, a stack of procedure file records, or even a stack of stacks. Ada also offers what Bertrand Meyer calls constrained genericity, in which a generic subprogram or package restricts its parameterized types by requiring the programmer to supply certain functions that operate on those types. For example, a generic sort routine can only be instantiated on types for which an ordering function, LessThan(), is defined. Another example is a matrix manipulation package that requires the programmer to specify addition (+) and multiplication (*) operators for the target data types, e.g., integers, reals, and complex numbers.

An ideal candidate for constrained genericity is the code that implements Sun MicroSystems' XDR protocol. XDR (eXternal Data Representation) defines standard representations for a number of different data types. For example, integers are 32 bits wide with the bytes ordered from most significant to least significant, floating point numbers follow the IEEE floating point format, etc. With the appropriate XDR conversion software on both sides, computers of dissimilar architectures can now exchange binary information in a standard way. All interprocess communications between programs in a TPOCC-based system are performed over network connections using the XDR protocol. Thus, the TPOCC processes can be distributed in an arbitrary topology on different machine types.

XDR-formatted data can also be exchanged via files or through shared memory, not just over network connections. Consequently, the communication medium serves as a useful abstraction for a generic XDR conversion package. Listing 8 presents a generic Ada package (with the specification and body combined) that handles XDR conversions on different types of data streams. Three routines are defined that encode various types of data for sending and decode the same types of data when received. To use the XDR package, the programmer must specify a type, STREAM_TYPE, that is used to identify communication channels of the desired stream type, and two low-level I/O routines for that stream type. For example, the XDR package might be instantiated for network connections by the following statement:

    package NET_XDR is new XDR (SOCKET_TYPE, NET_READ, NET_WRITE) ;

where a variable of SOCKET_TYPE is simply the file descriptor for an open network connection; NET_READ and NET_WRITE are functions that, respectively, read data from and write data to a socket connection. Instantiating the package for a shared memory stream would be accomplished by:

    package SHM_XDR is new XDR (SHM_ACCESS, SHM_READ, SHM_WRITE) ;

A variable of type SHM_ACCESS could be a structure containing the addresses of the first and last locations of a queue in shared memory. SHM_READ and SHM_WRITE would then adjust these pointers as XDR-formatted data is added to or removed from the queue.

The genericity of the XDR package is constrained because the package can only be instantiated for stream types having defined read and write functions. This is a purely syntactic constraint, since there are no semantic constraints placed on the I/O functions. Consequently, it would be perfectly legal to instantiate XDR for BOOLEAN streams, whatever those are:

    package NONSENSE_XDR is new XDR (BOOLEAN, NULL_READ, NULL_WRITE) ;

NULL_READ and NULL_WRITE are dummy functions that do nothing and return.

Unlike Ada, Eiffel does not directly support constrained genericity. Instead, the same effect is achieved using inheritance. Simulating constrained genericity using inheritance is accomplished in two easy steps:

  1. Define a deferred class for each parameterized type; the constraint functions for a type are embedded as functions (also deferred) in that type's class.
  2. Define the "generic" class as a normal class, written in terms of the deferred "parameter" classes and functions (from Step 1).

To instantiate the group of new classes, subclasses of the deferred "parameter" classes must be defined for the desired target types; the "generic" class can be used as is.

The Ada XDR package was converted to Eiffel by following the procedure above. First, the Ada package parameter, STREAM_TYPE, was represented in Eiffel by an abstract input/output object, IO_OBJECT, with two constraint functions, ReadData() and WriteData(). The "generic" XDR object class was then written using IO_OBJECTs and the deferred functions, ReadData() and WriteData().

To instantiate the Eiffel XDR "package" for a particular type of I/O, simply define a subclass of IO_OBJECT for the desired I/O mechanism; thanks to polymorphism and dynamic binding, no changes to the "generic" XDR class are required. Listing 9b presents NET_OBJECT, a subclass of IO_OBJECT; objects of this type read and write raw data over network connections. When coupled with a NET_OBJECT object, an XDR object will read and write data in XDR format on a network connection. The object hierarchy appears as follows:

                            IO_OBJECT               XDR
                             /     \
                    NET_OBJECT    SHM_OBJECT

The code fragment found in Listing 10b shows the interplay between a NET_OBJECT and an XDR object. NET_OBJECT's Create() function is called to establish a network connection; the resulting NET_OBJECT, stream, is passed to XDR's Create() function and an XDR object, handle, is created. An integer in XDR format is read from the network by calling handle's xdr_int() function; xdr_int(), in turn, calls stream's ReadData() function. In like manner, xdr_string() calls stream's WriteData() function to output an XDR string.

Interestingly, Sun's implementation of XDR (written in C) uses an object-based approach. An XDR data stream is identified by a handle, which is just a pointer to an XDR structure. This structure contains pointers to a number of primitive I/O functions: input N bytes from the data stream, output N bytes to the data stream, and several others. The various XDR routines (xdr_int(), xdr_string(), etc.) are written in terms of these primitive I/O functions (called indirectly through the handle structure) and have no concern for or access to the underlying data stream. As with X Windows widgets, varying the function pointers stored in the XDR handle varies the behavior of an XDR "object". Specific functions are available to create XDR handles for different types of I/O: xdrrec_create() for buffered network communications, xdrmem_create() for memory-to-memory transfers, and xdrstdio_create() for exchanging data via files.

The XDR object class described above is a relatively simple example of constrained genericity. Since the XDR class was defined in terms of its abstract parameter type, IO_OBJECT, there is no need to make changes to the XDR class when adding new forms of IO_OBJECTs, e.g., for file I/O, for message queues, etc. Other constrained generic classes are more complex, however. Suppose we wish to define an ORDERED_LIST class, each instance of which is a sorted list of items. ORDERED_LIST is a generic class with a single parameter, the type of the items, subject to the following constraint: any two items in the list must be capable of being compared.

Following the procedure for simulating constrained genericity with inheritance, we first define a deferred "parameter" class, COMPARABLE, with a deferred function, LessThan(), which compares two items. Subclasses of COMPARABLE (and their appropriate LessThan() functions) must be defined for the different types of elements that will be kept in ordered lists. Next, we define the ORDERED_LIST class itself, declaring the items in the list as COMPARABLEs (just like we did with the XDR class and IO_OBJECTs). The AddItem() function in this class takes one argument, a COMPARABLE object. Everything is just peachy until an errant program tries to add an orange to a list of apples. Since both apples and oranges are COMPARABLEs, AddItem() can't distinguish between the two and doesn't realize that comparing them is meaningless. The solution is to subclass both the "parameter" class and the "generic" class:

             COMPARABLE                 ORDERED_LIST
              /      \                   /       \
           APPLE    ORANGE    LIST_OF_APPLES    LIST_OF_ORANGES

In the ORDERED_LIST subclasses for apples and oranges, variables and functions declared of type COMPARABLE are now redeclared as APPLEs or ORANGEs. Thus, the AddItem() function in LIST_OF_APPLES is passed an APPLE object; the same function in LIST_OF_ORANGES is passed an ORANGE object.

Eiffel provides a mechanism called declaration by association, which minimizes the rewriting necessary when creating subclasses, both of generic classes and of non-generic classes. Declaration by association allows you to say that the type of a variable or function is like the type of another variable or function. To use this mechanism in ORDERED_LIST, for example, you would declare a single variable, say anchor, as being of type COMPARABLE; the remaining variables and functions of the same type are declared as "like anchor". To create the subclass LIST_OF_APPLES, you only need to redeclare anchor as an APPLE; the other, "anchored" variables and functions will automatically be retyped.

Why does Eiffel specifically support unconstrained genericity, but not constrained genericity? Apparently, the complexity of building constrained genericity into Eiffel outweighed any potential benefits. Furthermore, constrained "parameter" classes such as IO_OBJECT turn out to be useful abstractions in their own right.

Why support unconstrained genericity if it can be simulated using inheritance? Unconstrained genericity can indeed be simulated using inheritance, in the same manner as for constrained genericity. Since there are no constraints, the deferred, "parameter" classes are empty (no functions or attributes); in fact, each "parameter" class is essentially a universal class that can apply to any type of object. For example, if the generic STACK class was simulated using inheritance, a stack of procedure file records could no longer be declared simply as "STACK[PROC_FILE_RECORD]". Instead, a dummy class, STACKABLE, would be defined for STACK's type parameter and all potentially stackable objects (including primitive objects such as numbers and string) would have to be defined as subclasses of STACKABLE. The awkwardness of this solution and the frequency with which unconstrained genericity was used in practice inspired the development of a more convenient mechanism for unconstrained genericity in Eiffel.

(After writing the previous section on constrained genericity, I have since learned that later versions of Eiffel now support constrained generic objects. The ORDERED_LIST class can be declared as a true generic class:
    class ORDERED_LIST[T->COMPARABLE] ...
where "T->COMPARABLE" means that type parameter T must be a descendant of COMPARABLE. A list of apples would then be declared as "ORDERED_LIST[APPLE]". Although this eliminates the need for subclassing ORDERED_LIST, the need for COMPARABLE and its descendants remain.)

Generic object classes have not received the attention they deserve in object-oriented circles, largely because most OO research is performed using interpreted, untyped languages such as Smalltalk and the various flavors of object-oriented Lisp. In an untyped language, a class such as STACK is already generic: an element of a stack can be of any type. Depending on your application, however, this may be a mixed blessing, since additional effort is required to ensure that a stack is homogenous.

Among typed, object-oriented languages, Eiffel and Trellis support genericity; in the latter, a generic class is called a type generator for parameterized types. C++ will eventually support it, although the simpler forms of genericity can already be simulated using the text substitution capabilities of the C macro preprocessor.

 

Assertions

Eiffel supports the basic capabilities required for object-oriented programming: inheritance (both single and multiple), polymorphism, dynamic binding, and, for typed languages, genericity. In addition, Eiffel provides the programmer with a number of other capabilities, some unique to Eiffel. Foremost among these is the incorporation of assertions into Eiffel object classes. The support for assertions is a result of the important role abstract data types played in the design of the Eiffel language.

An abstract data type (ADT) is a description of a data structure. The data structure is essentially hidden inside a black box; the ADT defines the external interfaces to the data structure and the behavior of the data structure - as viewed solely through the external interfaces. In particular, an ADT says nothing about the implementation of its data structure. An abstract data type consists of 4 parts:

Types are the data types involved in this ADT. These include the ADT itself and any component data types.

Functions are the external interfaces to the data structure. A constructor function creates a new instance of one of the data types. An accessor function returns information about an existing instance of a data type. A transformer function modifies an existing instance of a data type. (Strictly speaking, a transformer function actually returns a modified copy of the original data structure.)

Preconditions are restrictions placed on the inputs to functions.

Axioms are conditions that must hold true after the application of a function to an instance of the data type. Axioms specify the behavior of the data structure in response to external stimuli (function calls!).

For example, the abstract data type specification for a spacecraft command buffer would have two types: the command buffer itself and the mission-specific commands. The functions that can be applied to a spacecraft command buffer include:

Preconditions on these functions would, for example, prevent adding a command to a full buffer or uplinking an empty buffer. Axioms for the spacecraft command buffer data type would specify the ordering of commands in the buffer, the order in which commands are uplinked, the automatic transmission of commands in one-step mode, the effects of cancelling a critical command, etc.

The important thing to remember about abstract data types is that they are abstract - there should be no mention of implementation details in the ADT specification. For example, the implementation of a spacecraft command buffer could be as primitive as a simple card file. When the operator enters a new command into the buffer, the computer prints out a 3"x5" index card containing the command information. Another operator then adds the card to a little card box (if the box was not already full) and manually sorts the cards in the box. When the first operator enters a /SEND directive to uplink the commands, the second operator phones up the receiving station and reads off the information on the cards in the box. The receiving station then uplinks the commands to the satellite, possibly by flipping thumbwheel switches on their transmitter, after which the control center operator can throw his/her index cards out. All the functions performed by the POCC operators meet the requirements imposed by the preconditions and axioms of the spacecraft command buffer's abstract data type.

As you might guess, abstract data types map naturally into Eiffel object classes:

                Abstract Data Type            Eiffel Class
                   Types            <------>  Classes
                   Functions        <------>  Exported Functions
                   Preconditions    <------>  Preconditions
                   Axioms           <------>  Postconditions and
                                              Class Invariants

Listing 11 presents the Eiffel class declaration for a simplified spacecraft command buffer. A function's preconditions, stated in the function's require clause, specify the conditions that must be met by clients invoking this function. Postconditions, expressed in a function's ensure clause, specify how the target object is transformed by calls to the function. The class invariants, found in the invariant clause at the end of the class declaration, are general assertions about the state of an object of the given class when the object is in a stable condition, e.g., after the object is created and after an external call to any function exported by the object. (Class invariants are essentially postconditions common to all exported functions.)

A distinctive feature of programming in Eiffel is that the preconditions and postconditions of a function represent a contract between a client invoking the function and the object servicing the function. If the client satisfies the requirements spelled out in the preconditions, the target object guarantees the results promised by the postconditions (and, implicitly, the class invariants). If the client fails to keep its half of the bargain, the target object is not bound to fulfill the other half.

Assertion checking can be enabled or disabled when an Eiffel program is compiled. For time-critical programs, checking is usually enabled during testing and disabled in the final product. If a precondition, postcondition, or class invariant fails during the execution of a program (and if assertion checking is enabled), an exception is generated. (Exceptions are described in greater detail later.) If the exception is not handled by the code, the program aborts with an error message indicating which assertion in what class was violated.

To prevent programs from unceremoniously crashing, clients of an object must assume the responsibility of verifying that all preconditions are satisfied before calling a function of that object. For example, a command processing program should explicitly check that the spacecraft command buffer is not full before adding a new command to it; the command buffer object should not have to make that check. On the surface, it would seem preferable to centralize the condition checking in the target class rather than forcing every client to perform its own checks. As Cline and Lea have pointed out, however, an optimizing compiler can eliminate unnecessary condition checks in the client, an optimization not available if the checks are confined to the target class. In the example above, for instance, there is no need to check that the command buffer is full when adding a new command immediately after clearing the buffer.

Like the preconditions and axioms of an abstract data type, the preconditions, postconditions, and class invariants of a class specify the behavior of objects belonging to that class. Furthermore, these assertions, inherited along with the attributes and functions of the class, provide semantic constraints for descendants of the class. In particular, a function redefined in a child class must not violate the conditions imposed on the original function in the parent class. Properly written assertions in a parent class thus prevent the subclassing of children that don't do what they're told. (Contrast Eiffel with Ada: in the earlier discussion of genericity, a generic Ada package, XDR, was legally instantiated as NONSENSE_XDR, with input/output routines that didn't do anything!)

Although a child class cannot violate the assertions of its parent, it can modify them within certain limits. Recall that assertions represent a contract between a client and a target object. Because of polymorphism and dynamic binding, the target object of a contract may belong to the parent class or to a descendant class. In order to satisfy the terms of the contract in this context, a subclass cannot:

Consequently, a redefined function in a child class can relax the preconditions of its parent (the preconditions defined in the child's function are ORed with those defined in the parent's function) or tighten up the postconditions (the postconditions defined in the child's function are ANDed with those defined in the parent's function).

Class invariants are assertions about an object class as a whole. Preconditions and postconditions are assertions about individual functions. Eiffel also allows the programmer to embed assertions in a class at the statement level. Simple check statements:

    check
        assertion 1 ;
        ...
        assertion N
    end ;

can be interleaved with normal program statements. When the execution of a program reaches a check statement, the enclosed assertions are evaluated; if any expression evaluates to false, an exception is raised.

One of the Holy Grails of computer science is the ability to formally prove a program correct, i.e., that the program meets its specifications. Although this remains an elusive goal for non- trivial programs, considerable success has been achieved in proving the correctness of loops (e.g., FORTRAN DO-loops and Pascal while-loops). A loop is correct if (i) the loop eventually terminates, and (ii) after it terminates, the loop has accomplished what it was supposed to.

To guarantee that a loop does not cycle forever, Eiffel lets you define a loop variant, an expression that initially evaluates to a positive integer and whose value is decremented by some amount on each loop iteration. Violations of the loop variant occur if its value is not decremented during an iteration or if its value drops below zero. Violating the loop variant produces an exception that, if not handled, aborts the program (very forcefully terminating the loop!).

Showing that a loop actually worked after it finished requires a loop invariant, an assertion about what the loop was supposed to do. A loop invariant must be true before the body of the loop is executed (but after any loop initialization), after each iteration, and after the loop terminates. Composing a logical assertion that captures the purpose of a loop and also holds true during intermediate iterations of the loop is, not surprisingly, a difficult exercise. Textbook examples of loop invariants are usually drawn from programs that compute mathematical functions. Unfortunately, mathematical functions are to formal proofs of program correctness what graphical objects are to object-oriented programming: it's not clear how to generalize from the obvious to the not-so-obvious.

Eiffel loops are constructed as follows:

    from loopInitialization
        invariant loopInvariant variant loopVariant
    until exitCondition loop
        ... body of loop ...
    end ;

The invariant and the variant are optional. Violating either, if present, causes an exception to be generated.

 

Exceptions

An exception is an abnormal event in a program. In some cases, exceptions are detected by the computer hardware and an interrupt is generated; exceptions of this type include: illegal memory references, arithmetic overflow and underflow, floating point processor errors, etc. Other exceptions are detected by software, either in the operating system or in an application program, and generate "software" interrupts; examples of these include: domain and range violations in a math library, broken I/O connections, etc. Responding to and handling exceptions is an important attribute of robust, reliable software.

The VAX/VMS operating system implements an elegant, hierarchical exception handling mechanism. A call stack maintains a record of the nested invocations of procedures (i.e., functions and subroutines). When a procedure (in any language) is called, a frame is pushed on the call stack. The frame contains the new procedure's local variables, the return address in the parent procedure, a link back to the parent procedure's stack frame, and a slot reserved for the address of a condition (exception) handler.

If an exception occurs during the execution of a procedure, the operating system checks if a condition handler has been established for the procedure. If not, the operating system checks the parent procedure for a condition handler and, if necessary, continues to "walk" back up the call stack until a procedure with a condition handler is found. (A "last-chance" handler is set up before a program begins executing.)

Once a condition handler is found, the operating system calls the handler routine (just like any other procedure), passing it information about the context in which the exception occurred. The condition handler, at its disgression, can handle the exception or resignal it up to higher-level condition handlers. If it decides to take care of the exception itself, the condition handler can fix up the error and let the program resume execution in the procedure where the exception originally occurred, or the call stack can be "unwound" to the point at which the condition handler was declared. Unwinding the stack effectively aborts execution in the "unwound" procedures and returns control to the parent of the procedure that declared the condition handler.

The following example presents a FORTRAN-style function that computes a pseudo-telemetry value; the function returns a VMS-compatible status flag indicating the successful completion of the computation. A condition handler is declared and used to prevent arithmetic errors from crashing the decommutation program. The function and its condition handler, written in pseudo-code, appear as follows:

    FUNCTION ComputePseudoPoint ()
        CALL Lib$Establish() to establish a condition handler.
        Perform processing.
        Return normal completion status (SS$_NORMAL).
    END

    FUNCTION ConditionHandler (exception, registers)
        IF exception is a condition I'm handling THEN
            Unwind the call stack and return exception (SS$_XXX) as an error status.
        ELSE
            Signal exception back up the call stack.
        ENDIF
    END

Assume that routine Decom() calls ComputePseudoPoint():

                                 Decom()
                                    |
                           ComputePseudopoint()
                                  / | \  --> ConditionHandler()
                               ... ... ...

If ComputePseudoPoint() completes normally, a status code indicating success is returned to Decom(). If an exception occurs in ComputePseudoPoint() (or in any routines it calls), ConditionHandler() is invoked and, through the magic of VMS exception handling, ComputePseudoPoint() and any descendant routines are popped from the call stack, the exception code is set up as the return value of ComputePseudoPoint(), and execution returns to the calling routine, Decom(). Decom() only sees a status code indicating success or failure returned by ComputePseudoPoint(); decom() is oblivious to the fact that an exception handler may or may not have been invoked.

Several points should be noted with regards to the VMS exception handling mechanism:

Exceptions can be handled with relative ease by programs running under VAX/VMS. In contrast, exceptions can be handled only with great difficulty in the UNIX/C environment. Under UNIX, exceptions are known as signals. A handler function can be assigned to a signal using the signal(3) system call; establishing one or more handlers for multiple signals requires multiple calls to signal(3).

When a signal occurs, the operating system calls the handler function, passing it the signal code and some implementation-dependent information about the cause of the signal. Once invoked, the signal handler can abort the program, ignore the signal, try to fix the error and continue processing, or escape from the current execution context. The latter approach, similar to unwinding the stack in VMS, is accomplished with a pair of system calls:

Typically, setjmp(3) is called by the routine that declares a signal handler. longjmp(3), called from within the signal handler, pops intervening functions from the call stack and returns control to the original routine.

The UNIX/C exception handling mechanism has the following characteristics:

The awkwardness of handling exceptions in the UNIX/C environment generally discourages programmers from doing so in their programs. It will come as no surpise that the exception handling facilities incorporated into newer languages and operating systems appear to follow the hierarchical VMS model.

Ada carries VMS-style exception handling down below the subprogram level to the block level. Exception handlers can be defined for packages, tasks, functions, procedures, and begin-end blocks. The exception handling code resides, not in a separate function, but in an exception clause at the end of the given program entity. Unlike their VMS counterparts, Ada exception handlers have only two processing options available: perform any clean-up chores and exit the function or block (or whatever), or resignal the exception. An Ada handler cannot cause execution to resume at the point where the exception occurred.

The pseudo-telemetry point calculation would, if coded in Ada, look somewhat like the following:

    function COMPUTE_PSEUDO_POINT return STATUS is
    begin
        ... perform processing ...
        return success ;
    exception
        when NUMERIC_ERROR =>
            return failure ;
        when others =>
            raise ;     -- Resignal exception.
    end ;

The Ada version of this function works just like its VMS FORTRAN cousin. If no exceptions occur, the function returns a status code indicating success. If a NUMERIC_ERROR exception occurs, the function returns a status code indicating failure. All other exceptions are resignalled up the call stack to higher-level exception handlers.

Although Bertrand Meyer originally considered exceptions a sign of poor programming (see his description of Eiffel in SIGPLAN Notices, 2/87), he eventually added exception handling to Eiffel. The syntax is very similar to Ada's, but the names have changed. A rescue clause found at the end of a function declaration or at the end of a class declaration contains code to handle exceptions. If an exception occurs in a function, the function's rescue clause is executed. If the function has no rescue clause, the class's rescue clause is executed. If neither the function nor the class have a rescue clause, the exception is raised in the client object (i.e., the object that invoked the function). The exception rises up through the run-time calling hierarchy (effectively unwinding the call stack) until a rescue clause that handles the exception is found.

After performing any preliminary processing (e.g., reporting errors, etc.), an Eiffel rescue clause has only two courses of action open to it: retry the function (with which the clause is associated) or resignal the exception to the client object. retrying a function literally restarts the function; control is transferred back to the beginning of the function. Local variables are not redeclared and reinitialized; they retain their values from the time of the exception (or later, if they're modified by the rescue clause). If a rescue clause decides not to handle an exception, it must restore the target object to a consistent state, i.e., one that satisfies the class invariant.

Because a rescue clause's choices are so limited, Eiffel programs that include exception handling tend to be less straightforward than the equivalent programs in Ada, as the Eiffel rewrite of our example function shows:

    ComputePseudoPoint: STATUS is
        local  errorOccurred: BOOLEAN ;
    do        -- Compute value of pseudo-telemetry point.
        if errorOccurred then
            Result := failure ;
        else
            ... perform processing ...
            Result := success ;
        endif ;
    rescue
        if exception = Numerical_error then
            errorOccurred := true ;
            retry ;
        end
    end ;

When ComputePseudoPoint() is first called, local variable errorOccurred is automatically initialized to false and the else block is executed. Normally, the function completes its processing and returns success as its result. If a numerical error occurs and an exception is raised, then the rescue clause sets the errorOccurred flag to true and restarts the function. This time through, the if block is executed and failure is returned to the client object.

The good news about Eiffel exception handling is that hierarchical exception handling is an integral part of the language. The bad news is that Eiffel's exception handling is implemented using C's ungainly setjmp(3)/longjmp(3) mechanism. The Eiffel compiler generates C source files, not assembly language or machine code; the host computer's C compiler builds the final executable program. Since the Eiffel compiler has no knowledge of or access to the architecture-specific features of the target system, it must rely on standard library functions such as setjmp(3) and longjmp(3) for exception handling. Unfortunately, Eiffel programs pay a heavy price, performance-wise, to preserve the portability of the Eiffel compiler. To make matters worse, it appears that every function, whether or not it has a rescue clause, has an implicit traceback handler established upon entry, thus incurring the cost of a call to setjmp(3). VAX afficionados may be forgiven a certain amount of smugness: VAX/VMS can generate traceback listings with no run-time overhead during normal processing.

Exception handling in other object-oriented languages typically follows the Ada model. Trellis, for instance, has except clauses. Smalltalk, on the other hand, uses a different approach. When an exception occurs in a method, an exception message is sent to the target object. If the target object's class has defined a method that responds to that exception message, then the method is invoked. Otherwise, a search is made up the inheritance hierarchy for a method that does handle the exception. Dony has proposed a hierarchical, invocation-based mechanism for exception handling in Smalltalk, one more along the lines of VMS/Ada/Eiffel exception handling. An exception handling mechanism is promised for C++, but, meanwhile, C++ programmers must make do with the standard UNIX/C exception handling facility: signal(3), setjmp(3), and longjmp(3).

 

Why Eiffel?

 

Why Not Eiffel?

This tutorial/proposal is not complete yet and I'm not sure if or when I'll finish it. Because of a number of problems I've read about on USENET, Eiffel is looking less and less likely as a candidate for TPOCC software development. Consequently, my enthusiasm for the language and for finishing this paper has waned.

I had planned to discuss the advantages of Eiffel with respect to other object-oriented languages like C++ and SmallTalk. C++ is probably the most popular OOP language at this time - unfortunately! C is a programming language that, despite its recent resurgence, should have been retired long ago. With its emphasis on pointers and user-controlled memory allocation, C makes for very efficient, but very buggy, programs. We TPOCCers had the time to leisurely immerse (mire) ourselves in the intricacies of C and gain some mastery of the language; other projects, on tight schedules, cannot afford to give their programmers that opportunity. C++ adds significant new capabilities to C, while at the same time retaining the significant handicaps of C. As Bertrand Meyer says, "hybrid languages give hybrid results".

On the other hand, Eiffel is not quite as efficient or as simple as Dr. Meyer's 1988 book suggests. (I should point out that this is no fault of the book's. The book is about object-oriented programming, not Eiffel, and more up-to-date references about Eiffel are available from the company who makes the compiler.) USENET mail messages reveal that a number of enhancements and extensions have been made to Eiffel since 1988, thus increasing the complexity of the language. Still, Eiffel is more readable and less complex than C++. Furthermore, as SunWorld remarked, Eiffel's "focus is the correctness and robustness of the target program". C++ can't claim that - there are too many C loopholes.

The USENET mail traffic also highlighted performance problems with Eiffel, a fact that diminishes Eiffel's potential as a language for POCC software development. The performance of Eiffel-generated code is not as much of a problem as it might seem; some of the existing non-real-time TPOCC applications written in C (e.g., the X Windows-based display program) are already notorious hogs of CPU cycles and memory! Nevertheless, Eiffel doesn't appear to be the appropriate language for developing real-time software such as telemetry decommutation, etc.

Fortunately, there is now an alternative to Eiffel. The International Computer Science Institute, of the University of California at Berkeley, is performing research in a number of areas such as computer vision, etc., which typically require large, complex programs that run fast. C was considered too unmanageable for the level of complexity they foresaw, so ICSI evaluated several object-oriented languages as suitable replacements. C++ and Objective-C were tried, but found wanting in their support of memory management and encapsulation. CLOS (an object-oriented LISP) was too slow. The Institute finally settled on Eiffel as the language of choice. However, they had the same experiences with Eiffel that others had: Eiffel was evolving into a semantically complex language and the execution speed of Eiffel programs was insufficient for the processing-intensive applications the Institute was developing.

At a dead end, ICSI did the only thing they could: they developed a new OOP language called Sather. Sather is an Eiffel look-alike that combines the best features of Eiffel and C++. In terms of conforming to advanced software engineering practices, Sather programs are comparable to Eiffel programs. In terms of performance, Sather programs are comparable to C++ programs and, in some cases, faster. This was mostly achieved by:

Like the Eiffel compiler, the Sather compiler is UNIX-based and generates C code. Thus, like Eiffel, Sather should be highly portable among UNIX platforms. Sather also has a flexible interface to the C language, so Sather programs can easily call existing C code and existing C programs can easily call Sather code.

The best thing about Sather is that it is free. The compiler source code, supporting libraries, and documentation were recently released by the International Computer Science Institute and are available via anonymous ftp from icsi.berkeley.edu.

 

Bibliography

  1. Association for Computing Machinery, Communications of the ACM, Vol. 33, No. 9, September 1990.
    A special issue on object-oriented design, with articles covering object-oriented design and programming in general and C++, Eiffel, and Trellis in particular.
  2. G. Booch and M. Vilot, "The Design of the C++ Booch Components", OOPSLA/ECOOP '90 Conference Proceedings, October 1990.
    Recounts their experiences porting the Booch Ada utilities to C++.
  3. M. Cline and D. Lea, "The Behavior of C++ Classes", SOOPPA '90 Conference Proceedings, September 1990.
    A description of A++ (Annotated C++): C++ with embedded assertions and axioms.
  4. C. Dony, "Exception Handling and Object-Oriented Programming: towards a synthesis.", OOPSLA/ECOOP '90 Conference Proceedings, October 1990.
    A general discussion of exception handling and a proposed mechanism for Smalltalk.
  5. M. Eriksson, "A Correct Example of Multiple Inheritance", ACM SIGPLAN Notices, Vol. 25, No. 7, July 1990.
    The title says it all.
  6. D. Ingalls, "A Simple Technique for Handling Multiple Polymorphism", OOPSLA '86 Conference Proceedings, November 1986.
    Multi-methods: how to handle M-by-N polymorphism in Smalltalk.
  7. R. Jellinghaus, "Eiffel Linda: An Object-Oriented Linda Dialect", ACM SIGPLAN Notices, Vol. 25, No. 12, December 1990.
    Integrating Linda, a concurrent programming language, with Eiffel, an object-oriented language.
  8. B. Leather, "After the Divorce: Reflections on Using Eiffel at Cognos", SOOPPA '90 Conference Proceedings, September 1990.
    A classic article about how a real company attempted to use Eiffel on a real project. The results were mixed. Noted gains in productivity were offset by problems with the compiler (an early version of Eiffel) and a lack of tools (debugger, etc.). Management problems contributed to the fiasco.
  9. C. Lim and A. Stolcke, Sather Language Design and Performance Evaluation, Computer Science Division, U.C. Berkeley, TR-91-034, May 1991.
    An in-depth evaluation of the performance of Sather-generated code. This report is included in the Sather distribution.
  10. B. Meyer, "Eiffel: Programming for Reusability and Extendability", ACM SIGPLAN Notices, Vol. 22, No. 2, February 1987.
    The first published description of Eiffel (although "Genericity vs. Inheritance" presented some aspects of the language). It was much simpler back then.
  11. B. Meyer, "Genericity vs. Inheritance", OOPSLA '86 Conference Proceedings, November 1986.
    Genericity and inheritance in Ada and Eiffel.
  12. B. Meyer, Object-oriented Software Construction, 1988.
    The eminently readable classic on object-oriented programming and Eiffel.
  13. S. Meyers, "Working with Object-Oriented Programs: The View from the Trenches is Not Always Pretty", SOOPPA '90 Conference Proceedings, September 1990.
    Real-life experience with object-oriented programs, or "Why did I have to trace through 9 object classes in two source files to figure out what one lousy function call does?"
  14. S. Omohundro, The Sather Language, 1991.
    A full description of Sather, an object-oriented programming language that combines the best features of Eiffel and C++. This manual is included in the Sather distribution.
  15. J. Palsberg and M. Schwartzbach, "Type Substitution in Object-Oriented Programming", OOPSLA/ECOOP '90 Conference Proceedings, October 1990.
    Treating any object class as a potentially generic object class.
  16. K. Polese and T. Goldstein, "OOP Language & Methodologies", SunWorld, Vol. 4,No. 5, May 1991.
    An overview of object-oriented programming.
  17. C. Schaffert, T. Cooper, B. Bullis, M. Kilian, and C. Wilpolt, "An Introduction to Trellis/Owl", OOPSLA '86 Conference Proceedings, November 1986.
    Another strongly-typed OOP language.
  18. E. Seidewitz (Code 552, GSFC), "Object-Oriented Programming in Smalltalk and Ada", OOPSLA '87 Conference Proceedings, October 1987.
    One of Goddard's own!
  19. E. Seidewitz (Code 552, GSFC), "Object-Oriented Programming Through Type Extension in Ada 9X", Ada Letters, Vol. 11, No. 2, March/April 1991.
    Do object-oriented capabilities need to be added to Ada? This article discusses how many features of object-oriented programming can be simulated using extensible record structures and access types.
  20. P. Wegner, "Workshop on Object-Oriented Programming, ECOOP 1987", ACM SIGPLAN Notices, Vol. 23, No. 1, January 1988.
    A panel discussion on various facets of object-oriented programming.
 

Listing 1: Creating Laurel and Hardy Objects

    class HUMAN export
        Eat, Sleep, IsEating, IsSleeping
    feature

        eating: BOOLEAN ;    -- TRUE if eating,
                             -- FALSE if sleeping.

        Eat is               -- Begin eating.
        do
            eating := true
        end ;

        Sleep is             -- Fall asleep.
        do
            eating := false
        end ;

        IsEating: BOOLEAN is
        do
            Result := eating
        end ;

        IsSleeping: BOOLEAN is
        do
            Result := not eating
        end ;

    end

    Laurel, Hardy: HUMAN ;

    Laurel.Create ;          -- Instances of humans.
    Hardy.Create ;

    Laurel.Sleep ;           -- Stanley sleeps ...
    Hardy.Eat ;              -- while Ollie eats.
 

Listing 2: Generic Minor Frame Template

    deferred class MNF export
        Decommutate, Discard, Fill, MnfID, Save
    feature

        buffer: ARRAY[INTEGER] ;

        Create (size: INTEGER) is      -- Allocate buffer.
        do
            buffer.Create (1, size) ;
        end ;

        Decommutate is                 -- Extract data.
        do
            TableDrivenExtraction ;
        end ;

        Discard is                     -- Discard contents.
        do
            deferred
        end ;

        Fill is                        -- Fill with data.
        do
            deferred
        end ;

        MnfID: INTEGER is             -- Returns MNF ID.
        do
            deferred
        end ;

        Save is                        -- Record in history file.
        do
            deferred
        end ;

        TableDrivenExtraction is     -- Private routine.
        do
            ... extract data using DBMS-defined parameters ...
        end ;

    end
 

Listing 3: Mission-Specific Minor Frame Object Class

    class mission_MNF export
        Decommutate, Discard, Fill, MnfID, Save
    inherit
        MNF rename Create as MnfCreate,
                   Decommutate as MnfDecommutate
            redefine decommutate ;
    feature

        sizeOfMNF: INTEGER is 128 ;    -- 128 bytes per MNF.
        locationOfID: INTEGER is 9 ;   -- MNF ID in byte 9.

        Create is                      -- Allocate buffer.
        do
            MnfCreate (sizeOfMNF) ;
        end ;

        Decommutate is                 -- Extract data.
        do
            ReverseFrame ;
            MnfDecommutate ;
        end ;

        Discard is                     -- Discard contents.
        do
            -- Do nothing.
        end ;

        Fill is                        -- Fill with data.
        do
            ... get next minor frame from TNIF
                and store in frame buffer ...
        end ;

        MnfID: INTEGER is             -- Returns MNF ID.
        do
            Result := buffer.item (locationOfID) ;
        end ;

        Save is                        -- Record in history file.
        do
            ... write current contents of buffer to log file ...
        end ;

        ReverseFrame is                -- Private routine.
        do
            ... reverse bytes in frame buffer ...
        end ;

    end
 

Listing 4: Simultaneous Processing of MSS and TM Data

    cycle is
        thisMNF: MNF ;                     -- Generic MNF variable.
    do
        from startOfPass until endOfPass loop
            thisMNF := GetNextMNF ;        -- MSS or TM.
            thisMNF.Decommutate ;
        end ;
    end ;

    GetNextMNF: LANDSAT_MNF is
    do
        ... returns next available MNF, either MSS or TM ...
    end ;
 

Listing 5: Abstract Display Device Class

    class DISPLAY_DEVICE export
        DisplayPoint, DisplayLine,
        DisplayCircle, DisplayRectangle
    feature

        DisplayPoint (point: POINT_OBJECT) is
        do
            deferred         -- Plot a point.
        end ;

        DisplayLine (line: LINE_OBJECT) is
        do
            deferred         -- Draw a line.
        end ;

        DisplayCircle (circle: CIRCLE_OBJECT) is
        do
            deferred         -- Draw a circle.
        end ;

        DisplayRectangle (rectangle: RECTANGLE_OBJECT) is
        do                   -- Draw a rectangle.
            DisplayLine (rectangle.side_1) ;
            DisplayLine (rectangle.side_2) ;
            DisplayLine (rectangle.side_3) ;
            DisplayLine (rectangle.side_4) ;
        end ;

    end
 

Listing 6: Generic Stack Class

    class STACK[T] export
        Push, Pop, Top, IsEmpty
    feature

        IsEmpty: BOOLEAN is
        do
            ... returns TRUE if stack is empty ...
        end ;

        Pop is
        do
            ... pops top object from stack ...
        end ;

        Push (item: T) is
        do
            ... pushes item on stack ...
        end ;

        Top: T is
        do
            ... returns top object on stack ...
        end ;

    end
 

Listing 7a: Procedure File Record

    class PROC_FILE_RECORD export
        CurrentLine, GetLine
    feature
        currentLine: INTEGER ;
        text: ARRAY[STRING] ;

        Create (name: STRING) is
        do                      -- Create record.
            ReadFile (name) ;
            currentLine := 1 ;
        end ;

        GetLine: STRING is
        do                      -- Return text of current line.
            Result := text.item (currentLine) ;
            currentLine := currentLine + 1 ;
        end ;

        ReadFile (name: STRING) is
        do
            ... create TEXT array and read file into it ...
        end ;

    end ;
 

Listing 7b: Procedure File Table

    class PROC_FILE_TABLE export
        GetLine, GoToLine, PopFile, PushFile
    feature

        procedureStack: STACK[PROC_FILE_RECORD] ;
        top: PROC_FILE_RECORD ;

        Create is
        do                   -- Create table.
            procedureStack.Create ;
        end ;

        GetLine: STRING is
        do                   -- Returns text of current line.
            Result := top.GetLine ;
        end ;

        GoToLine (lineNumber: INTEGER) is
        do                   -- Position to line N in current file.
            top.currentLine := lineNumber ;
        end ;

        PopFile is
        do                   -- Pops current file from top of stack.
            procedureStack.Pop ;
            top := procedureStack.Top ;
        end ;

        PushFile (name: STRING) is
        do                   -- Push new file on stack.
            top.Create (name) ;
            procedureStack.Push (top) ;
        end ;

    end
 

Listing 8: Generic XDR I/O Package in Ada

    --
    -- Generic XDR I/O Package.
    --

    generic
        type STREAM_TYPE is (<>) ;
        with procedure READ_DATA (STREAM: STREAM_TYPE,
                                  BUFFER: ADDRESS, SIZE: NATURAL) ;
        with procedure WRITE_DATA (STREAM: STREAM_TYPE,
                                   BUFFER: ADDRESS, SIZE: NATURAL) ;
    package XDR is              --
                                -- Read/write an integer.
                                --
        procedure XDR_INT (IS_READ: BOOLEAN, STREAM: STREAM_TYPE,
                           VALUE: in out INTEGER) is
        begin
            if (IS_READ) then
                READ_DATA (STREAM, VALUE'ADDRESS, VALUE'SIZE/8) ;
            else
                WRITE_DATA (STREAM, VALUE'ADDRESS, VALUE'SIZE/8) ;
            end if ;
        end ;                   --
                                -- Read/write a floating point number.
                                --
        procedure XDR_FLOAT (IS_READ: BOOLEAN, STREAM: STREAM_TYPE,
                             VALUE: in out FLOAT) is
        begin
            if (IS_READ) then
                READ_DATA (STREAM, VALUE'ADDRESS, VALUE'SIZE/8) ;
            else
                WRITE_DATA (STREAM, VALUE'ADDRESS, VALUE'SIZE/8) ;
            end if ;
        end ;                   --
                                -- Read/write a character string.
                                --
        procedure XDR_STRING (IS_READ: BOOLEAN, STREAM: STREAM_TYPE,
                              VALUE: in out STRING) is
        begin
            if (IS_READ) then
                READ_DATA (STREAM, VALUE'ADDRESS, VALUE'LENGTH) ;
            else
                WRITE_DATA (STREAM, VALUE'ADDRESS, VALUE'LENGTH) ;
            end if ;
        end ;

    end XDR ;
 

Listing 9a: Abstract Class for Input/Output Objects

    deferred class IO_OBJECT export
        ReadData, WriteData
    feature
        ReadData (buffer: ADDRESS, size: INTEGER) is
            deferred
        end ;
        WriteData (buffer: ADDRESS, size: INTEGER) is
            deferred
        end ;
    end
 

Listing 9b: Subclass for Network I/O Objects

    class NET_OBJECT export
        ReadData, WriteData
    inherit
        IO_OBJECT
    feature
        socket: INTEGER ;    -- File descriptor for socket connection.

        Create (host: STRING, port: STRING) is
        do
            ... Establish socket connection to HOST/PORT ...
        end ;

        ReadData (buffer: ADDRESS, size: INTEGER) is
        do
            ... Read data from socket connection ...
        end ;

        WriteData (buffer: ADDRESS, size: INTEGER) is
        do
            ... Write data to socket connection ...
        end ;

    end
 

Listing 10a: XDR Object Class

    class XDR export
        xdr_int, xdr_float, xdr_string
    feature
        stream: IO_OBJECT ;

        Create (ioStream: IO_OBJECT) is
        do
            stream := ioStream ;
        end ;

        xdr_int (isRead: BOOLEAN, value: INTEGER) is
        do
            if isRead then
                stream.ReadData (value.address, value.size) ;
            else
                stream.WriteData (value.address, value.size) ;
            endif ;
        end ;

        ... and so on for XDR_FLOAT and XDR_STRING ...

    end
 

Listing 10b: Using an XDR Object on the Network

    handle: XDR ;
    stream: NET_OBJECT ;
    value: INTEGER ;


    stream.Create ("host", "port") ;        -- Establish connection.
    handle.Create (stream) ;                -- Create XDR stream.


    handle.xdr_int (true, value) ;          -- Read integer.
    handle.xdr_string (false, "Hello!") ;   -- Write string.
 

Listing 11: Spacecraft Command Buffer Object Class

    class CommandBuffer export
        AddCommand, ClearBuffer, UplinkCommands
    feature

        maxCommands: INTEGER ;
        numCommands: INTEGER ;
        buffer: ARRAY[SC_COMMAND] ;

        Create (maxNumCommands: INTEGER) is
        do              -- Create command buffer.
            maxCommands := maxNumCommands ;
            numCommands := 0 ;
        end ;

        AddCommand (command: SC_COMMAND) is
        require
            numCommands < maxCommands
        do              -- Add command to buffer.
            numCommands := numCommands + 1 ;
            buffer.Enter (numCommands, command) ;
        end ;

        ClearBuffer is
        require
            numCommands > 0
        do
            numCommands := 0 ;
        end ;

        UplinkCommands is
        require
            numCommands > 0
        do
            ... uplink commands in buffer to spacecraft ...
            ClearBuffer ;
        ensure
            numCommands = 0
        end ;

    invariant

        ... class invariants ...

    end

Alex Measday  /  E-mail