Robert "Rock" Howard is president of Tower Technology Corporation, creator of the TowerEiffel system, and chairman of NICE, the Eiffel Consortium. You can contact him at 2701 Stratford Drive, Austin, TX 78746 or at [email protected].
With software complexity mushrooming because of ever-increasing user expectations, programmers should avoid "accidental" complexities imposed by languages and tools whenever possible. The Eiffel programming language, perhaps more than others, removes unnecessary complexities without limiting the ability to express the inherent complexity of software.
Eiffel combines object-oriented (OO) capabilities with a unique focus on software "correctness" and reusability. Eiffel programs can express abstractions in a clear manner using a syntax that's simple to learn and use. Eiffel can be used for OO design-specification, or as a full-fledged, software-engineering tool.
Eiffel is a class-based language that reinforces OO design. It includes multiple and repeated inheritance, selective exporting, strong type checking, parameterized classes, dynamic binding, garbage collection, feature renaming, exception handling, and persistency. Eiffel is implemented via sophisticated compilers that perform dependency analysis and optimization. To speed the development cycle, some implementations include interpretation.
Since its introduction in 1986, Eiffel has attracted an international following in academia and engineering and is used extensively by in-house and commercial software-development projects. Eiffel is the only OO language outside of Smalltalk and C++ with multiple commercial implementations. The Eiffel trademark is owned by the Nonprofit International Consortium for Eiffel (NICE), an independent consortium that administers and enforces language and core library standards.
Eiffel's major benefit is the reduction of software-maintenance costs. This comes about in two ways. First, Eiffel supports the development of correct software via the use of semiformal assertion technology. Second, Eiffel supports the development and use of reusable software. These concepts are intimately linked since software correctness is a prerequisite for effective software reuse.
Eiffel is a "pure" object-oriented language. Every value is either an object, a reference to an object, or void. (Void denotes the state of a reference that is not currently attached to an object.) While Eiffel syntax resemble Pascal, it was designed as a class-based, statically-typed object-oriented language.
All Eiffel classes implicitly inherit from class ANY, and include both shallow and deep copying, cloning, comparison, and so on. Eiffel supports a clean mechanism allowing you to easily add capabilities to this class.
Eiffel has no formal notion of pointers. Conceptually, all feature arguments are object references. The compiler can "decide" to implement a feature call in an appropriate manner--given the actual type of the objects used as arguments. Compared to languages where there are choices between pointer and reference semantics, Eiffel's uniformity in calling conventions simplifies class interfaces.
Eiffel has standard mechanisms for interfacing with C, including calling C from Eiffel, and Eiffel from C. Some implementations include extensions that allow the use of C macros or inline C within designated Eiffel routines. Come compiler vendors also include mechanisms for accessing Eiffel object data or features from C.
Eiffel implementations come with a variety of kernel classes handing basic types--integers, reals, floats, Booleans, strings, arrays, bits, and so on. The compiler knows about these types and can produce optimized code when they're used. Other kernel classes are basic I/O, standard files, and exceptions. NICE is working to standardize the kernel classes.
It is easy to add persistency to Eiffel. Interactive Software Engineering (Goleta, CA) provides the STORABLE class that allows for persistence to classes that inherit from it. Tower Technology (Austin, TX) provides a library that can encode any Eiffel object into a heterogeneous, packed binary format that can be decoded into a separate process on another machine, or at a future time. SIG Computer (Braunfels, Germany) has specialized FILE classes used for simple persistency.
Improving Software Correctness
Eiffel eliminates the possibility of entire categories of errors--memory management errors, unintended side effects caused by poor encapsulation, bad links from erroneous makefiles, improper routine dispatching due to type errors or confusion, and the like. Eiffel also "attacks" errors in application logic and semantics via its advanced assertion technology. Assertions support the contract metaphor, aid feature and class comprehensibility, interact with the exception-handling capabilities, and improve confidence in system reliability. Debugging time, especially defect location, is usually cut drastically when assertions are used in Eiffel classes.
Assertions in Eiffel come in two flavors--preconditions and postconditions. (Class invariants, loop invariants, loop variants, and checks are also supported, but for the sake of this argument you can lump them in with postconditions.) Preconditions require that arguments provided to feature calls are acceptable, and that the object is in a state such that the feature call can be handled. Postconditions ensure that a chunk of code performed correctly, provided an acceptable result, and left the object in a correct state.
The preconditions, postconditions, and class invariants are treated as inherent parts of the class interface and are retained under inheritance to enforce required behavior even for subclasses. Eiffel's automatic interface extraction tool treats assertions as equally important as routine signatures. Unlike other languages, semantic assumptions expressed via assertions are explicit and public.
When a run-time error occurs, the Eiffel exception-handling capability kicks in. If no handler is designated for a given assertion failure, it's handled by the Eiffel run-time system, which dumps trace and other information. Precondition failures denote errors in the feature caller, while postconditions denote errors in the feature body. Thus the liberal use of assertions usually immediately isolates many run-time defects. (For further discussion of software correctness, see "Writing Correct Software with Eiffel" by Bertrand Meyer, DDJ, December 1989.)
Eiffel supports the reuse of classes via inheritance, parameterization, and composition. The inheritance mechanism allows renaming and redefinition of features so that derived classes can be easily specialized. Parameterization goes beyond simple template construction as type parameters can be constrained. This allows the constraint class's capabilities to be used within the enclosing parameterized class, enabling the implementation of high-level abstract data types. Eiffel also includes an indexing clause for locating reusable classes.
Exploring Eiffel
To examine Eiffel's syntax, let's begin by looking at the linked list example (see the Eiffel code on page 122, which is part of the article, "Comparing Object-oriented Languages"). At first glance, the syntax seems similar to Pascal--assignment via :=, If statements, routine declarations, and the optional semicolon statement separators are familiar elements borrowed from Pascal. The loop construct is demonstrated in class MY_LIST. The from/until/loop/end pattern can also be supplemented by optional loop invariant and loop variant assertions.
Distinguishing Eiffel from Pascal are the clauses introduced by the inherit, creation, and feature keywords. The optional inherit clause lists the class or classes that are inherited. In this clause, it's possible to rename individual features and otherwise join classes so that naming conflicts don't occur. The optional creation clause lists procedures that are available to initialize a newly-created object. The feature clause specifies the export status for one or more Eiffel attributes and/or routines grouped within it. (Eiffel uses the term "feature" to denote attributes or routine since, in some cases, they may be substituted for each other.)
Although not specified by the language definition, current Eiffel implementations expect that each class definition resides in a separate file with a suffix of .e. The name of the class need not match the file name, although that's the convention. Related classes are often grouped together in directories called "clusters." An important cluster is the "kernel" cluster that's provided with each Eiffel implementation. The kernel includes the low-level base classes, including the basic types--integer, real, double, character, array, string, file, and others.
A configuration file names the clusters where classes may be found for building an executable. It also names a class as the top-level class, and a feature as the initial feature to call within the top-level class. Various debugging options and other system-building parameters are specified. Interactive Software Engineering, and Tower Technology use the "LACE" configuration file format while SIG Computer uses the "pdl" format. (NICE will likely standardize on a single format in the future.)
To build the sample Eiffel system, the configuration file specifies DRIVER as the top-level class and make as the initial feature to call. The code for make in class DRIVER performs the same work as the C++ main() function.
The !! syntax denotes object creation. Some classes denote one or more procedures as "creation procedures" by naming them in the creation clause near the beginning of the class. If a class has no creation procedure, then the object is created without a call (for example, !!list1). If a class has at least one creation procedure, then creation requires a call such as (!!n1.set( 10 ).
The set_data routine in class ELEMENT can be used for either creating an instance of ELEMENT, or as a normal feature call. If required, export controls can be used to make a routine available only for creation.
The Eiffel class PRINTABLE replaces MyListData in the C++ code example. Note that PRINTABLE is marked deferred so that no objects of type PRINTABLE can be created. Instead, it's used as an abstract data type and inherited from it to get reliable polymorphic behavior. Also, the class LIST is a general-purpose version of MY_LIST. LIST takes a generic parameter named T. To use LIST, you declare it with an actual generic parameter; for instance, this_list : LIST(HAT);. Thus this_list holds objects of type HAT and/or objects that inherit from class HAT.
Class MY-LIST also has a generic parameter, but there's a restraint on its parameter [T->PRINTABLE]. This means that only objects that inherit from PRINTABLE are allowed to be used wherever T is used within the class. The result is that the compiler ensures that the call to print_self (which was defined as a deferred feature in class PRINTABLE) will always work.
More Code Notes
Eiffel assertions can have optional labels that are echoed to the standard error output if the exception caused by an assertion failure is not handled; see the invariant clause of class LIST.
Every attribute and routine is defined within the scope of a feature clause that denotes its export status. Feature [NONE] is equivalent to private. Feature [ANY] is equivalent to public since all Eiffel classes automatically inherit from class ANY. Feature [LIST} in class ELEMENT is an example of selective exporting. Only class LIST and classes that inherit from it may invoke these features.
Eiffel automatically initializes all fields to default values, so some of the initialization logic from the C++ code is not necessary.
The I/O calls (put_string, put_int, put_newline, and put_char) are from the SIG's Eiffel/S BASIC_IO class. Interactive Software's Eiffel 3 has a slightly different I/O library. Tower uses either ISE or Eiffel/S style. NICE is developing a single I/O standard and other core capabilities.
Extending the Linked List Example
Although the Eiffel linked list implementation is wordier than the C++ implementation, it's safer, more understandable, and does much more. For example, it includes a general-purpose list, not one specialized for printable objects. Furthermore, the specialized class MY-LIST is safer since nonconforming objects can't be added to it.
The routines in Example 1 are additions to class LIST for removing items. Eiffel will reclaim an item's memory as is appropriate. Doing this in C++ is painful because you must build or include a garbage collector or a sophisticated reference counting system to know when to call destructors safely.
The routine in Example 2 can be added to class LIST to return individual items. In Eiffel, there are two approaches to using this capability. One approach shown in Example 3 is to fetch the item as conforming to a type that includes the needed capabilities and then rely on dynamic typing. The other approach is shown in Example 4 and uses an assignment attempt. This will assign an object only if it conforms to the target reference. Otherwise the reference is set to VOID.
These examples show the Eiffel objects retain type information. Because C++ objects don't retain type information, the examples are hard to duplicate. Instead, C++ has the dangerous type coercion capability that can't match the safety and flexibility of these examples.
Development Automation
According to the Eiffel philosophy, compilers should take on many of the automatable tasks involved in building applications. For example, replying on programmers for dependency-analysis information (makefiles) is a waste of time and a source of problems. Requiring the maintenance of separate class interface files, including the complex include file invocation order, is another nonproductive effort.
Eiffel compilers take on much of the burden of performing system-wide and local optimizations. For example, all Eiffel features are "virtual" as far as the Eiffel programmer is concerned. The Eiffel compiler locates all appropriate opportunities for in-lining or removing unnecessary code and eliminating unnecessary dynamic binding. Programmers focus optimization efforts on choosing proper data structures and streamlining object interactions. The compiler also eliminates dead code, permitting you to focus on data structures and object interactions.
Eiffel compilers take on the task of fully checking for correct type usage, without imposing a confusing or limited type system on you. In Eiffel, subclassing and subtyping are one and the same, making the use of inheritance particularly powerful and easy. Eiffel supports the more natural, covariant type system that allows classes to be easily specialized in concert.
Final Thoughts
Eiffel encourages you to use languages and tools appropriately; that is, to use an OO language for implementing OO designs. You can then use a portable assembler such as C for low-level optimizations and operating-system interfaces, or an AI tool or language where a logic-based approach is needed. As long as the languages and tools can interface appropriately, this approach yields better results than the use of hybrid languages or tools.
Eiffel's support for semiformal, parameterizable specifications allow the construction of robust, reusable software components. The clear syntax couples with the elucidation of assumptions via assertions makes it easier to share these components with confidence. The result is that the real benefits of object-oriented programming are more fully realized.
Example 1:
is_empty : BOOLEAN is -- return TRUE if LIST is empty do Result := ( head = void ) end ; -- is_empty remove is -- remove the first list item require not_empty: not is_empty do head := head.next ; if head = void then tail := void ; end; end ; -- remove
Example 2:
item : T is -- return the head of the list require not is_empty do Result := head end ; -- item
Example 3:
local this_list : MY_LIST[PRINTABLE] ; pr : PRINTABLE ; do -- create the list !!this_list ; -- add items to the list (not shown) ... -- fetch an item from the list if not this_list.is_empty then pr := this_list.item ; pr.print_self ; -- dynamic binding still works! end
Example 4:
local this_list : MY_LIST[PRINTABLE] ; pt : MY_POINT do -- create the list !!this_list ; -- add items to the list ... (not shown) -- fetch an item from the list pt ?= this_list.item if pt /= void then -- it was a MY_POINT pt.print_self ; end ;
EIFFEL SOURCE CODE EXAMPLE class DRIVER -- In Eiffel, the top level driver is an object, too. inherit BASIC_IO creation make feature {ANY} make is -- run this test driver local list1, list2 : MY_LIST[PRINTABLE] ; n1, n2 : MY_NUMBER ; -- a kind of PRINTABLE p1, p2 : MY_POINT ; -- also a kinf of PRINTABLE do -- create the various objects !!list1 ; !!list2 ; !!n1.set( 10 ) ; !!n2.set( 20 ) ; !!p1.set( 2, 3 ) ; !!p2.set( 4, 5 ) ; list1.add_to_list( n1 ) ; list1.add_to_list( n2 ) ; list1.add_to_list( p1 ) ; list2.add_to_list( n2 ) ; -- objects can be in more than one list list2.add_to_list( p1 ) ; list2.add_to_list( p2 ) ; list2.add_to_list( list1 ) ; -- list 1 is an element of list 2 put_string( "list1:%N" ) ; list1.print_self ; put_string( "list2:%N" ) ; list2.print_self ; end -- make end -- DRIVER deferred class PRINTABLE -- insures that 'print_self' is implemented inherit BASIC_IO feature {ANY} print_self is -- print yourself deferred end -- print_self end -- PRINTABLE class MY_NUMBER -- holds and can print an integer inherit PRINTABLE creation set feature {ANY} value : INTEGER ; set( new_value : INTEGER ) is -- set this number do value := new_value ; end ; -- make print_self is -- print the value do put_string( "Number: " ) ; put_int( value ) ; put_newline ; end ; -- print_self end -- MY_NUMBER class MY_POINT -- holds and can print an x,y pair inherit PRINTABLE creation set feature {ANY} x, y : INTEGER ; set( new_x : INTEGER; new_y : INTEGER ) is -- set this point do x := new_x ; y := new_y end ; -- set print_self is -- print the value do put_string( "Point: " ) ; put_int( x ) ; put_char( ',' ) ; put_int( y ) ; put_newline ; end ; -- print_self end -- MY_POINT class ELEMENT[T] -- holds an object reference and a -- single link to another ELEMENT[T] creation set_data feature {LIST} data : T ; next : ELEMENT[T] ; set_data( new_data : T ) is -- set data to the new_data do data := new_data end ; -- set_data set_next( new_next : ELEMENT[T] ) is -- set next to the element do next := new_next end ; -- set_next end -- class ELEMENT class LIST[T] -- a generic linked list class feature {ANY} add_to_list( data : T ) is -- add to the end of the list local new_element : ELEMENT[T] ; do !!new_element.set_data( data ) ; if head = void then head := new_element ; else tail.set_next( new_element ) ; end tail := new_element ; end ; -- add_to_list feature {NONE} head, tail : ELEMENT[T] ; invariant tail_next_is_void: tail.next = void ; tail_void_when_head_void: head = void implies tail = void end -- LIST class MY_LIST[T->PRINTABLE] -- A printable list which holds printable data inherit LIST[T] PRINTABLE feature {ANY} print_self is -- print the list elements local el : ELEMENT[T] ; do from el := head until el = void loop el.data.print_self el := el.next ; end ; end ; -- print_self end -- MY_LIST
Copyright © 1993, Dr. Dobb's Journal