Dan introduces us to the Eiffel language, a fairly new Object-Oriented language designed to streamline the software engineeering process.
In the late 1970s, when I was a journeyman laboring over the macro assembler language, a simple, elegant, and expressive language called C emerged from the structured programming tradition. My friends advised me to forget C, to learn a “real programming language” like PL/1 or Ada, available on many more of the important platforms, and with commercial staying power due to the support of the government and major corporations. Ignoring this advice, I concentrated on C. Some of you may agree my choice was the right one for its time.
Now, more than a decade later, another simple, elegant, and expressive language has emerged, this time from the object-oriented tradition. With roots in Simula, beholden to no backwards compatibility issues, the Eiffel programming language was designed from scratch by Bertrand Meyer and his colleagues for the purpose of enabling the construction of robust, general, reusable software components. With commercial support from a few vendors, availability on several significant platforms, and some success stories in development of major commercial applications, this language bears careful observation.
I have known of Eiffel since 1989, when I read Dr. Meyer's book, Object Oriented Software Construction (Prentice Hall, 1988). I was impressed with the language, much as I had been impressed with C when I read Kernighan and Ritchie's classic, The C Programming Language (Prentice Hall, 1988), ten years earlier. The opportunity to give Eiffel a try presented itself with the Linux port announced in 1994 by Bertrand Meyer's company, Interactive Software Engineering of Goleta, California.
So what is all this noise about object-oriented programming, and what sets Eiffel apart from the pack of object-oriented languages?
Much of the promise shown by object-oriented software construction may be attributed to the possibility of code reuse, particularly the reuse of software components.
By reuse, I mean the incorporation of previously written software unmodified into new programs. In the US we have a saying: “if it ain't broke, don't fix it.” This folk wisdom highlights the result of research studies which indicate the great hazard of introducing new problems when working on existing software. Ideally, it should be possible to write something once, then put it on ice to be reused but never modified, except to fix bugs and perhaps to add new features. Existing users should be protected, if possible, from effects of such changes.
Unfortunately reuse has succeeded in only limited ways so far. For example, the Unix C library or the various widget libraries used in constructing Graphical User Interfaces are widely used across many platforms. These notwithstanding, most of the significant computer programs produced today contain great volumes of handcrafted code. The difficulty of producing software components with enough built—in flexibility is prominent—and a great part of this difficulty must be ascribed to language issues.
The Eiffel programming language was designed to promote re-use. Bertrand Meyer recounts in the preface to Eiffel: The Libraries (Prentice Hall, in press) how he started out to write reusable components and how he came to abandon the attempt to use existing languages and instead wrote a language for the purpose.
Compiled and strongly typed, with genericity (templates), polymorphism, dynamic binding, exceptions, garbage collection, a genuinely useful implementation of multiple inheritance, and unique handling of assertions, Eiffel should be considered a contender on purely technical grounds.
Eiffel provides assertions as language primitives that furnish both in-line design documents and optional runtime error checking. Assertions are inherited. This serves to guarantee that descendents will live up to or exceed their ancestors' promises. Use of these assertions is a part of what is called “design by contract”. These represent an application of responsibility-driven design at a language level.
There are also other factors:
Eiffel has a published, non-proprietary design, coordinated by a nonprofit consortium whose decisions all existing vendors agree to observe.
Simple and consistent syntax makes Eiffel an easy language to learn. You will find no dense nests of parentheses, asterisks, brackets, or ampersands in this language. If you delight in special cases and obscure exceptions to rules, with attendant language primitives just for handling these things, Eiffel is not the language for you.
Precedence of arithmetic operators gives the order of evaluation of mathematical expressions you would expect, unless your native programming language is Smalltalk, Lisp, or Forth.
Eiffel is available on a wide range of platforms.
The design of Eiffel has been placed in the public domain by Interactive Software Engineering, Bertrand Meyer's company. The Eiffel trademark is owned by NICE, the Nonprofit International Consortium for Eiffel, which has been liberal in bestowing use of the trademark. A validation suite will be available from NICE later this year. Major Eiffel vendors and users, including representatives from corporations and academia, are represented on NICE. Membership is open to any interested party. Proposals of NICE are published on the newsgroup comp.lang.eiffel. Anybody may participate in the ensuing discussion.
The official language definition is Eiffel: The Language, By Bertrand Meyer (Prentice Hall, 1992). Almost 600 pages long, this volume contains a precise definition of the language, many examples, and a great deal of discussion. The formal syntax definition occupies only eight pages.
NICE is in the process of standardizing the libraries. PELKS, the Proposed Eiffel Library Kernel Standard, is in the final stages of adoption, even as the vendors hasten to bring their own class libraries into line with it. Other libraries may follow.
Eiffel is available or announced from a number of vendors on a roster of operating systems or platforms including Windows 3.1, VMS, SunOS, Solaris, Ultrix, OSF/1, DG Aviion, IBM RS/6000, Silicon Graphics, Macintosh, OS-2, and NEXTSTEP, as well as Linux.
Many Linux users are interested in seeing vendor interest in our operating system, and a few astute vendors are beginning to join the fold. It seems not too surprising that vendors of a language that is in many respects years ahead of the pack should also be astute enough to recognize the nature of the Linux community. And they have—all four Eiffel vendors offer Linux ports.
Object-oriented programming draws on just a few main ideas. I will talk about three of the important ones and illustrate their realization in the Eiffel language.
The first important idea is encapsulation: the packaging of data with means to manipulate it. Such a package, written in a programming language is a class, but an instance of a class in execution (or in storage) is an object.
In Eiffel, everything exists within a class. There are no external variables or routines. A class has features. Features in turn are either attributes or routines.
Attributes store values, including references to objects. They may be constant or variable.
Routines do things. Routines are either procedures or functions. Functions return results and are not supposed to change system state. Procedures change system state but return nothing.
All features, even constant or variable attributes, are said to be “called”. This is perhaps less strange than it might seem, for in Eiffel, a call to a function with no arguments is written the same as a call to an attribute. If in some class you write:
that := the.other
the.other may be either a function or an attribute, in this context it makes no difference.
So, you encapsulate data and the routines that manipulate it in a class. Assertions, mentioned previously, are also parts of a class and serve to express class preconditions, constraints and invariants.
The second important idea is inheritance.
Once you have a class, which describes what you know about the how and why of some sort of object, it may further benefit you to derive a new class from it, with additions and variations, without touching the program code in the original class. Inheritance is a mechanism that allows this.
For example, you might derive BEE from INSECT. Many features of BEE would inherit directly from INSECT, some features would be modified, and BEE would provide a few new features of its own.
A rule of thumb, called the “is-a” rule, furnishes one way to determine whether A might usefully inherit from B. Examine the sentence “A is a B”. Does it make sense? It should if A is a reasonable candidate to inherit from B. For example, “BEE is an INSECT” passes this test, so BEE might inherit from INSECT. Then, INSECT will be ancestor of BEE, and BEE will be a descendent INSECT.
The “has-a” rule furnishes a contrast. If “A has a B” makes more sense than “A is a B”, it may be prudent to let A reference or have a feature of type B, rather than inheriting from B. “BEE has a STINGER” makes more sense than “BEE is a STINGER” or for that matter “STINGER is a BEE”. Therefore, class BEE should have a feature of type STINGER. That makes BEE a client of STINGER, and STINGER a supplier to BEE.
The client-supplier relationship offers a client less detailed control than a descendent. A client may use or not use a feature of a supplier, but it cannot redefine such a feature. Many of a supplier's features may be hidden from a client, while they will be visible to a descendent. The positive side of this is that the client will be relatively unaffected by details of a supplier's implementation and less likely to be impacted by changes in a supplier.
Both client and inheritance relationships can facilitate software reuse. The traditional function call is more akin to the client relationship, and many attempts at reuse in the past, prior to object-oriented approaches, have made use of the function call. However, we still find ourselves writing and rewriting familiar pieces of functional code too complex to make good candidates for library routines.
The implementation details of inheritance may seriously affect its suitability as a mechanism for reuse. In the ideal implementation, problems arising in descendent classes could always be resolved there. Unfortunately, with many languages, problems arising in descendent classes require changes to ancestors.
This becomes more true with multiple inheritance, a technique by which a class may enjoy, or perhaps not enjoy, multiple ancestors. This technique is a powerful one, but it is unavailable in many object-oriented languages and discouraged in most of the rest. Among languages that have reached commercial viability, Eiffel offers a superior implementation of multiple inheritance.
In its broad sense, this indicates a situation where a simple request may elicit different but not entirely inconsistent responses, depending on the target of the request. These responses may be arrived at by entirely different means.
For example, you might have classes that look in part like this:
class INSECT -- Description of a standard -- insect. ... feature flee is do -- How a standard -- insect flees. ... end; -- flee end class BEE inherit INSECT redefine flee end; ... feature flee is do -- How a bee flees. ... end; --flee end class COCKROACH inherit INSECT redefine flee end; ... feature flee is do -- How a cockroach flees. ... end; -- flee end class WATERBUG inherit INSECT redefine flee end; ... feature flee is -- How a water bug flees. ... end; -- flee end
Then, you might have examples of BEE, COCKROACH, and WATERBUG bound to references of INSECT:
-- Define references to an -- INSECT and to a BEE. insect:INSECT; bob:BEE; -- Bind some particular -- insect to the reference insect := bugs.get -- Now you have a BEE, a -- COCKROACH or a WATERBUG. -- Make it flee after its own -- fashion, be it that of BEE, -- COCKROACH, or WATERBUG. -- This is a polymorphic call, -- as the code executed will -- depend on the type of the -- object bound to insect. insect.flee; -- You can't make a WATERBUG -- collect pollen. -- For this you need a BEE, and -- a trial assignment is -- available to assign objects -- that might conform to the -- type of a reference. bob ?= insect; -- Then maybe you can make bob -- the bee collect pollen. -- If he isn't a BEE or a -- conforming type, bob is Void. if bob /= Void then bob.collect_pollen end;
Another sort of polymorphism, sometimes called parametric polymorphism, is supported in Eiffel as genericity.
The final sort of polymorphism I'll mention is found as function overloading in other languages. Here, a function may be defined multiple times, with different types and numbers of arguments. When the function is called, the actual function invoked depends on the argument list.
Function overloading is not implemented in Eiffel. Workarounds are present, and arithmetic operations are handled as special cases, but the general case of function overloading is felt by the designers of Eiffel to be too full of potential ambiguities, type-checking failures, complications, and interactions to be worthwhile just now. A lively thread on this topic is seen from time to time on the newsgroup comp.lang.eiffel.
Multiple inheritance is a big win under Eiffel. If you have dealt with this in other languages you may be surprised. Multiple inheritance entails many sticky problems, including name collisions and complications of repeat inheritance, and in most other languages is best avoided whenever possible.
When two classes inherited by common descendent have different features of the same name, a name collision obtains.
When a feature is inherited more than once from a common ancestor along two or more inheritance paths, you have repeat inheritance. This may cause a name collision and also raises practical problems, such as which repeated feature to use in a polymorphic call.
Most object-oriented languages do not attempt multiple-inheritance. The literature is full of elaborate explanations why. This is sad. Eiffel demonstrates that multiple inheritance need not be difficult or complex, and it can also yield some quite practical results.
Eiffel provides an implementation of multiple inheritance which minimizes the adverse effects of name collisions and repeat inheritance complications. As is typical in Eiffel, the solution to one problem helps solve another problem. In Eiffel, a name inherited from an ancestor may be revised in a descendent using a rename clause. Eliminating name collisions then merely involves giving a new name to one or both of the colliding features. The feature is unaffected, except that it is known in the renaming class and any descendents of that class by its new name.
Supposing we have a class named SOME_OTHER that inherits two entirely different features both called put from two ancestors named FIRST_CLASS and BOWSER:
class SOME_OTHER inherit FIRST_CLASS rename put as first_put end; BOWSER rename put as bowser_put end; ... end
then in some client class we may have feature what_now:SOME_OTHER
-- We may invoke the feature -- named put from either of its -- originating classes what_now.first__put(this); what_now.bowser_put(that);
Repeat inheritance is only slightly more complex. In the simple case, repeat inheritance is just a by-product of the class inheritance structure you have chosen. Perhaps some of the classes inherited from the class library have common ancestors—this is almost certain. Your responsibility is easy: do nothing. The compiler eliminates the duplicates, and your class has only a single copy of each inherited feature, regardless of how many different ways a feature might come to be present.
In the less common situation, you might want two copies of a feature, for instance put, from an ancestor. A class fragment exhibiting this situation might look something like this:
class SOME_OTHER inherit FIRST_CLASS rename put as first_copy select -- Resolve an ambiguity. first_copy end; FIRST_CLASS rename put as second_copy end;
The select clause serves to resolve an ambiguity. Suppose you reference an object of type FIRST_CLASS and you happen to invoke a feature known in FIRST_CLASS as put.
SOME_OTHER inherits FIRST_CLASS twice, and because of the renaming, there are two possible answers to the question, “What is the name of put in SOME_OTHER?” The following code fragment illustrates:
-- Declare a reference of type FIRST_CLASS -- then attach an object of type SOME_OTHER, -- a descendent of type FIRST_CLASS, using a -- creation procedure of SOME_OTHER called "make" this:FIRST_CLASS; !SOME_OTHER!this.make; -- When you try to invoke put, -- without select it is not clear what you mean. -- Could mean first_copy, could mean second_copy. this.put;
The select clause removes the ambiguity by saying, “When invoking this duplicated feature under its ancestral name, select this copy.” Note that Eiffel brings a thorny problem, repeat inheritance, to the resolution you might hope for. Duplicates are simply eliminated, with no further intervention. If you wish to duplicate-a much less common situation—you can do this using quite simple syntax. Finally, features of the same name that are distinct may be renamed so that both are available, or the unwanted feature may be undefined. pb In each case, the mechanisms provided are simple and local to the class where the problem arises. No revisions to ancestors are required. This is no accident. A major thrust in the design of this language was to eliminate the occasions for revisions to ancestors. Reuse is served here by localizing any adaptations required by repeat inheritance and name collisions in the class where they are encountered. The ancestors are not broken, so they require no fixing. If they are not fixed, they will not have bugs introduced, and other clients or descendents of the ancestor classes will not be affected by changes, bug-inducing or otherwise, that might otherwise be required in the absence of suitable means to resolve conflicts in the descendent.
Suppose you need a class to manipulate a structured collection of objects—an array of INSECTs, a parse tree, a hashed list of sales prospects, or some such thing?
Some object-oriented languages furnish a general approach. You construct a template for, say, a LINKED_LIST or an ARRAY. You then use this template with some arguments indicating the classes to be used in constructing the particular LINKED_LIST or whatever.
In Eiffel, this capability is called genericity, and the templates are generic classes. As usual, this is done in a way that does the job and yet is so simple, it seems effortless.
Suppose you are writing an ant hill. First you need some ants.
class ANT inherit INSECT -- A basic ant. It has features to crawl, -- forage, dig, tend the young, and so on. ... end class CARPENTER_ANT inherit ANT redefine -- Redefine some things. -- These chew on your house and your apple -- tree. ... end; ... end class ARMY_ANT inherit ANT redefine -- These are always on the go. -- You hope they don't stop by your place -- for dinner. ... end; ... end
Now you are ready for ant hills. Let us suppose you already have some class that models insect societies. Its header might look like this
class INSECT_SOCIETY[G->INSECT] ...
which indicates that an INSECT_SOCIETY may be formed using a parameter that conforms to INSECT. Loosely, this means any descendent INSECT will do. anthill:INSECT_SOCIETY[ANT] declares a reference to an INSECT_SOCIETY containing ants. This reference may then be attached an INSECT_SOCIETY containing CARPENTER_ANT, ARMY_ANT, or any other descendent of ANT we have defined. In fact, this allows us to reference an anthill comprising more than one kind of ant, which is convenient, as some anthills may contain more than one kind of ant.
In writing a specific container class, for example, we may wish to take advantage of things we know about insects in the features of the container class. It would never do, in such a situation, for example, to enter an object of class DOG or HAMMER into this container. The type-safe mechanism for doing this is called constrained genericity and is illustrated above in the header line for class INSECT_SOCIETY.
The Eiffel programming language offers power, simplicity, strong type checking, and numerous other amenities. With an open specification for both the language and the kernel libraries, and support from multiple vendors, Eiffel now stands poised to take off.
According to one vendor, most interest lately has come from people who are turning to Eiffel having used C++ for some years and who have become convinced that the training costs and the complexity of that language are not justified by the features provided.
The more adventurous among us who have a thirst to tackle an object-oriented programming language unhindered by excess baggage from the past ways of doing things may wish to further explore this language.
In my next article, I'll write more about ISE Eiffel and the compiler and tools from Tower Technology of Austin, Texas. I'll also offer a few thoughts about how to get started with this language.