Eiffel for Native Speakers of C++

. WITH THE RECENT RELEASE in January of a GNU compiler for the language, Eiffel has begun to move out of obscurity into public awareness. Its usefulness as a language for designing large-scale, object-oriented systems is now gaining recognition in the programming community—recognition some consider long overdue.

The language was first conceived in 1985 by Dr. Bertrand Meyer, a noted expert and pioneer in the field of object-oriented programming. The idea was to provide a clean language to express complex, powerful, object-oriented designs in a simple, elegant manner; and many agree that it has succeeded remarkably. Over the last decade, Eiffel has matured into a language especially well-suited to large, evolving projects involving many team members with varying expertise and levels of proficiency.

Design By Contract: The Heart of Eiffel
At the center of the Eiffel mindset is the concept of Design by Contract (or DBC). The idea behind DBC is that every usage of a piece of software is regarded as a contract: The "client" agrees to fulfill certain criteria, and in return, the "supplier" agrees to ensure that certain things will occur. It is this notion of contract that allows the complexity of large systems to be managed, because each component has well-delineated responsibilities.

In particular, a method (known in Eiffel parlance as a feature, a term that includes functions, procedures, and attributes) can have a precondition and a postcondition associated with it. If the client satisfies a feature's precondition and then calls that feature, the client can rely on the supplier to satisfy the postcondition. For example, the following would be typical Eiffel code


class COUNTER

feature

   count: INTEGER

   increment_by (inc: INTEGER) is
       require
          inc > 0
       do
         -- Implementation goes here
       ensure
         count = old count + inc
       end
end
Perhaps in this example the contract is overkill, but it illustrates a few points. First, suppose that when increment_by is called, the computer detects that the precondition (the requirement that inc > 0) does not hold. In that case, we can be sure that there is a bug in the caller, and not in increment_by. On the other hand, if the precondition does hold, and the postcondition (count = old count + inc) fails, we know the bug actually is in increment_by. By placing the blame for an error on one piece of code, bugs can be localized quickly.

Of course, this technique is not unique to Eiffel. This exact scheme can be implemented in C++ using the assert macro. What's different is that the contract metaphor makes it clear that all instances of COUNTER should obey these rules. Any client using any COUNTER should be able to rely on this contract. Specifically, this means that the contract must be upheld by all subclasses of COUNTER.

This requirement is supported directly in Eiffel: Subclasses may redefine a method, but may not strengthen the precondition or weaken the postcondition. (Translation: They cannot require more from the client, nor promise less.) The idea is that if you agree to pay $20 for a lube job on your car, you will be satisfied if the mechanic does a complete tune-up, including a lube job, and charges only $10. By requiring less and ensuring more, he will have more than met your requirements.

Preconditions and postconditions are examples of assertions, a term that has slightly different connotations in the context of DBC than it does in normal C++ programming practice. "Assertion" commonly refers to a sanity check inserted into code to make sure that certain conditions hold. In DBC, such sanity checks still exist, but the term "assertions" refers more generally to rules that specify the interface between two pieces of code by delineating their respective responsibilities.

Another kind of assertion is the class invariant, which states some fact about a class that clients can rely upon at all times. Conceptually, a class invariant is a fact that is always true; however, the invariant cannot hold true all the time, because methods of the class must be free to manipulate its instances in arbitrary ways, as long as the object reaches a consistent (i.e., invariant-satisfying) state before the method is complete.

Thus, the question of when the class invariant holds is answered based on the notion of the externally observable state of an object. As long as program control is inside an object, its class invariant need not hold, because clients cannot test the properties of the object until they get control. The invariant need only hold when the object first gets control, and again when it relinquishes control. This rule provides the flexibility necessary to allow methods to call each other in order to operate on the object in whatever manner is necessary, and yet it maintains the integrity of the object from the point of view of outside observers.

(It may have occurred to some readers that multithreading removes the uniqueness of program control, and thus may allow observers to measure the state of an object even when that object is in the middle of an operation. Discussion of multithreading is beyond the scope of this article, but suffice it to say that a mechanism exists that provides simple, elegant, safe multithreading while still preserving the notion of externally visible object integrity.)

Command-Query Separation
The mindset of DBC leads to a coding practice called Command-Query Separation (or CQS). The rule here is simple: No routine that returns a value can have side effects. While it seems rather draconian at first, this practice leads to simple, clear, well-specified class interfaces. It allows any query to be called from within assertions in order to specify its properties. (Besides the theoretical reasons for keeping side effects out of assertions—which are meant to be declarative in nature—there is the practical motivation that disabling assertions for efficiency should not affect the semantics of the program.)

As an example of CQS, imagine a class for processing the results of a database query:


class DB_RESULT

feature

    next_row: DB_ROW is
        -- Each call returns the next row from the results
        -- Void result means no more rows
        do
           -- Implementation goes here
        end

end
This class seems simple enough: When a database query returns a result, we can read those results a row at a time until there are no more results to fetch, at which point it returns void, the Eiffel equivalent of a null pointer. However, there are no assertions in this class, so Design by Contract is not being used, and as such, the client of this class has no guarantee that this class (or any of its subclasses) does what he wants, or even what the comments say it does.

Employing CQS directly, we immediately come to this situation:


class DB_RESULT

feature

 current_row: DB_ROW
   -- Void means no more rows

 fetch_row is
   -- Each call puts the next row from the results in current_row
      do
         -- Implementation goes here
      end

end
This is not all that much of an improvement per se, because there is still no contract between DB_RESULT and its clients. However, simply by separating the command fetch_row from the query current_row, we have set the stage for a clear delineation of responsibility between DB_RESULT and its clients.

The place to concentrate here is on the postconditions. They answer the question, "What can the client expect this routine to do?" Here is another version of DB_RESULT with a meaningful postcondition, and its associated queries:


class DB_RESULT

feature

   current_row: DB_ROW

   current_row_num: INTEGER

   past_end: BOOLEAN

   fetch_row is
       do
          -- Implementation goes here
       ensure
          current_row_num = old current_row_num + 1;
          not past_end implies current_row /= void;
          past_end implies current_row = void;
       end

end

Notice how the comments have disappeared. They are now unnecessary because the postcondition tells exactly what the class is going to do. It is possible, of course, for a subclass to be purposely deceitful and, for instance, increment current_row_num without actually returning the next row; but short of this kind of deliberate sabotage, the contract will ensure that the client's expectations are fulfilled.

However, this is still not complete: What happens if fetch_row is called when past_end is already true? We have to decide what is the appropriate course of action in this case. Should fetch_row:

  • Do nothing?
  • Display an error message?
  • Restart from the beginning of the result set?
  • Throw an exception?
It seems that among these choices, the only one that is even close to satisfactory is to throw an exception. The other choices make assumptions about what the client wants done in exceptional circumstances. Throwing an exception, though, can be time consuming at runtime, and incurs other costs, such as enlargement of the executable (due to extra code necessary to throw and catch the exception).

In the spirit of DBC, it seems reasonable simply to require the client never to call fetch_row when past_end is true. It is a judgment call as to whether this places undue burden on the client, but in this case it seems sensible to add not past_end to the precondition:


fetch_row is
    require
       not past_end;
    do
       -- Implementation goes here
    ensure
       current_row_num = old current_row_num + 1;
       past_end = (current_row = void);
    end
Notice that the postcondition has been collapsed into a more concise, equivalent form. This form even suggests an implementation for the past_end query. In addition, assertions referring only to queries of the class (rather than, say, the arguments or return value of a routine) are normally placed in the class invariant, as a matter of style. So, the final form of the class looks like this:

class DB_RESULT

feature

    current_row: DB_ROW

    current_row_num: INTEGER

    past_end: BOOLEAN is
        do
          Result := current_row = void
        end

    fetch_row is
        require
           not past_end;
        do
           -- Implementation goes here
        ensure
           current_row_num = old current_row_num + 1;
        end

invariant
    past_end = (current_row = void);
    current_row_num >= 1;

end
This version of the class is substantially longer than the original (twice the number of nonblank lines), which had no command-query separation or design by contract. However, this is because it contains more information, and is far more specific about the functioning of the class without constraining its implementation. The user of this class can be confident that the client code will still work properly when a subclass of DB_RESULT is substituted, because Eiffel ensures that the subclass will fulfill the contracts specified by the superclass.

One other interesting note: The past_end query has changed from an attribute into a function. One might be concerned that this might break clients as it would in C++, because those clients would need to add "()" to every usage of that query. However, this is not the case in Eiffel. No distinction is made between attribute accesses and function calls from the client's point of view—neither of these requires "()"—so the designer of the supplier is free to choose the appropriate implementation without fear of unnecessarily disturbing clients. Clients of a class cannot assign values to its attributes directly, so in effect, Eiffel has taken responsibility for generating accessor functions automatically for every public attribute.

The Simplicity of Multiple Inheritance
There is another excellent aspect of Eiffel that makes it powerful and easy to use: its treatment of multiple inheritance. As C++ programmers, many of us may have come to consider multiple inheritance as something to be used sparingly, due to its inherent complexity. As a matter of fact, the designers of Java decided not to support multiple inheritance in its full form, apparently for exactly this reason. It may be surprising, then, that Eiffel has managed to provide a simple and powerful solution to the multiple inheritance problem, based on the simple concept of feature renaming.

The way it works is that when one class inherits from another, it may rename any feature it chooses. It will be the same feature, but it will simply be referred to by a different name. With multiple inheritance, this is a simple, effective way to avoid name clashes; but there's even more to it than that.

After all is said and done, a class will have a collection of features—some inherited from various classes, and some declared in the class itself—and a collection of associated feature names. As long as all feature names correspond uniquely with some feature, the class is legal. (There are other consistency rules, but they are fairly straightforward.)

One rather impressive consequence of this technique is related to the issue of feature replication versus sharing. In C++, if one class inherits repeatedly from another, it can either get two copies of that other class' features, or by using virtual inheritance, it can merge the inheritance structure into a diamond shape so that it gets just one copy of those features. The decision is made at the granularity of the class—that is, entire classes are merged or replicated—and it is made in the superclasses of the actual class in which the merging or replication is to take place.

In the Eiffel model, the choice between merging and replication is made at the feature granularity. For instance, a FURNITURE class may have features called price and size. It may have subclasses called SOFA and BED, and when a descendant SOFA_BED is created, it may merge the price feature, because the item has only one price, but it may replicate the size feature into sofa_size and bed_size, because the size of the item depends on the purpose for which it is to be used.

In addition, the Eiffel model allows the replication/merging decision to be made at the SOFA_BED level of the class hierarchy. The C++ mechanism would require the decision at the level of the SOFA and BED classes, which would have to inherit virtually from FURNITURE. Not only does this mean that the designers of SOFA and BED would have to have foreseen the need for a SOFA_BED class, but also that all subclasses of SOFA and BED must abide by the same replication/merging decision as SOFA_BED.

The Eiffel multiple inheritance mechanism is powerful, local (in the sense that all decisions about the combining of superclasses appear in the class in which the combining occurs), and intuitive. It provides proof by example that multiple inheritance need not be a complicated or dangerous undertaking.

Conclusion
As a C++ programmer, I found the simplicity and power of Eiffel appealing. Having now used Eiffel for some time, I can say that my expectations of the language have generally been satisfied. With the exception of a few minor irritations, such as verbosity due to lack of overloading (see sidebar), it has proven to be a useful language for building reusable class libraries.

The compilers and environments for Eiffel are also becoming more usable every day. When I first became interested in Eiffel, I was disappointed by the lack of good supporting environments. But now, with ISE's environment, designed by Bertrand Meyer himself (among others); Object Tools' Visual Eiffel for Windows, which produces 32-bit native code; Object Tools' Eiffel/S for CodeWarrior on the Macintosh; and the GNU SmallEiffel compiler with its extraordinary optimizations and portability, there are plenty of choices for those who want to try out something new. All four of these products are either free or have free trial versions that are available on the Internet.

In short, Eiffel is definitely worth a try. Even if you never end up using it, it is one of those languages that might just change the way you think about programming, simply by having learned it.

Eiffel on the Web
If you'd like to try Eiffel, there are many options available:


Patrick Doyle is a student of Computer Engineering at the University of Toronto. He is also responsible for research and development and special projects for Minotaur Software Ltd. He can be contacted at patrick@minotaursoftware.com.

Culture Shock

Despite the power and simplicity of Eiffel, there are a few aspects of the language that a C++ programmer may find unfamiliar or disagreeable. It would be unfortunate if aversion to such language features were to deter a programmer from investigating the language, so in hopes of preventing unpleasant surprises, some of the things that a C++ programmer might find most objectionable are listed below.

They are mostly syntactic issues, rather than conceptual limitations of the language, and as such can normally be overlooked in favor of Eiffel's other positive features:

  • Pascal-like keywords. Many C and C++ programmers like the conciseness of using symbols instead of keywords. Eiffel, on the other hand, uses full words and no abbreviations. It uses a do end pair instead of curly braces. It uses inherit instead of a colon. It uses character instead of char, and interger instead of int. In short, you will find nothing in Eiffel whose sole purpose is to reduce your keystroke count.

       Frankly, the Eiffel philosophy is that this doesn't matter. A line of code is written only once, yet is read an unlimited number of times. Of the total effort involved in programming, a minuscule amount of time can be attributed to actually keying in the code; accordingly, all effort expended in designing Eiffel syntax went into making it readable, rather than writeable. (Besides, modern editors can help by filling in keywords automatically, so even the keystroke count is often not an issue.)

  • No overloading. Eiffel does not allow a feature to be overloaded on the basis of the types of its arguments. This sometimes leads to rather artificial manual overloading, as found in the standard IO routines (put_string, put_character, put_integer).

       However, overloading was not omitted by accident. Not only was it considered by Eiffel's designer to be of questionable value and not without costs, but it also would unduly complicate the inheritance mechanism, which relies on unique names to identify each feature of a class. Again, it comes down to favoring readability over writeability and eliminating ambiguity.

  • No nested classes. A class cannot be declared within the scope of another class, nor within a routine. This is not an oversight, but a deliberate decision. Classes are meant to be reusable modules, and if one class is written inside another, then it has such a strong dependency with the enclosing class that it cannot possibly be reused independently.

       In C++, nested classes are merely a scope issue, rather than a semantic issue. Thus, if C++-style nested classes are desired, they can be achieved merely by prepending the enclosing class' name to that of the nested class, and access to the "enclosing" class' features can be granted through selective export.

  • No "private" features. In Eiffel, all instances of a class are also considered instances of all its subclasses. In accordance with this philosophy, anything available to a class is available to all its subclasses. Thus, while Eiffel supports the equivalent of "protected" features, there are no "private" features.

       Again, this is a deliberate omission. It is based on the idea that reusability is maximized when classes can be modified with the utmost flexibility without actually altering the original class text. By providing subclasses access to everything within a superclass, the subclasses are given the greatest flexibility in adapting the existing class to new uses.

       The "private" mechanism can be simulated using a client-supplier relation with a third class. This solution is more verbose, but preserves the conceptual simplicity of the Eiffel class system. Such a solution is possible through Eiffel's selective export mechanism, which provides precise control over which features are available to which classes. Thus, in practice, the lack of a "private" mechanism does not cause much of a problem.

However, there are also features that C++ programmers may welcome with open arms:

  • No header files or #include statements. If you want to use a class, just use it. The location of the files that describe the classes is an administrative issue that is outside the language proper. It is typically handled by special configuration files or command-line options, similar to the way include and library paths might be handled in a C++ environment.

  • Simplicity of multiple inheritance. The difficulties associated with multiple inheritance in C++ are well-known. In contrast, Eiffel's mechanism is simple and powerful, and is based on feature renaming.

  • Support for Design by Contract. Design by Contract is a technique for delineating the responsibilities of different classes in the system. It is a simple, yet powerful tool that makes large-scale software design more manageable. Eiffel is one of the few languages (Sather and Blue, both based on Eiffel, are the only others known to the author) that supports DBC directly.

  • Garbage collection. Like Java, Eiffel is based on the assumption that the underlying runtime environment will take care of freeing unused memory. This allows the developer to focus on solving application-related problems rather than dealing with memory management issues whenever such issues can be handled automatically. Also, through a standard class called MEMORY, Eiffel provides control over the garbage collector, to ensure that memory management is handled in a manner that is appropriate to the application.

  • Integration with C. Despite the fact that Eiffel bears no resemblance to C whatsoever, it is surprisingly simple to interface C code with Eiffel. External C routines can be wrapped in an Eiffel class declaration, and then calling C from Eiffel and Eiffel from C is fairly straightforward.

  • Compilation to JVM. There is at least one Eiffel compiler (the free SmallEiffel compiler) that compiles Eiffel into Java Virtual Machine bytecodes. Also, using the same mechanism as for interfacing with C, it is possible to call external Java routines from Eiffel code.

Featured

Upcoming Events

AppTrends

Sign up for our newsletter.

I agree to this site's Privacy Policy.