Eiffel is designed to build large, reliable, object-oriented systems so that even our small greeting requires the scaffolding of a fully fledged class. To run the program, enter the code in a file called hello.e (same base name as the class, but lowercase), compile it with co-compile -o hello hello.e, and start the resulting executable hello.
class HELLO create make feature make is do print("Hello World%N") end end
We have to grasp a number of concepts before understanding this program. First, Eiffel, like most object-oriented languages, talks about a system rather than a program. A system is a collection of classes (similar to a Smalltalk image although the latter is more a collection of objects with classes being special objects). To tell Eiffel where to start, we normally have to define a root class. Since our system contains only the HELLO class, this is not necessary.
Eiffel calls members of a class (attributes and methods) features. Classes mainly consist of feature definitions introduced with the keyword feature. In the example, we define a single method called make which prints the message.
This does not explain yet, how the method gets executed. When starting a system, Eiffel creates an instance of the root class, and that's where the create (or synonymously creation) statement comes in. It tells the compiler that the make method is a creation procedure (in other languages called "constructor") with no arguments which must be called when instantiating an instance of the class. Hence, when starting our "hello" application, Eiffel creates an instance of the root class HELLO and calls the constructor method make which prints the message.
Calling the constructor method make is just a convention. Any other name is syntactically just as fine. Note that, as another style convention, Eiffel always uses underscore characters to separate the parts of multi-word identifiers. Features and variables are always lowercase, classes uppercase, and constants start with an uppercase letter.
Local variables of a method are declared in advance in the optional local section of a method. Eiffel being an explicitly typed language lets us specify the type of each variable using a colon and the name of the type.
class ARITHMETIC creation make feature make is local i: INTEGER x: DOUBLE do i := 50 x := 1.5 + 3 * 2.0^3 + i print("x=" + x.to_string + "%N") end end
In the example, we use the two build-in types INTEGER and DOUBLE which correspond to C's int and double, respectively. The variables are all initialized automatically to a default value corresponding to their type. For numerical types, this is zero.
As for the variable declaration, Eiffel follows the Pascal syntax for the assignment operator (I still remember how unintuitive C's use of equal operator for assignment appeared to me when moving from Pascal/Modula to C). Arithmetical expression work as expected including the correct preferences, automatic conversion from integer to double, and the power operator ^.
There are a few details in the print statement which we have not seen before. First, the statement prints three strings which are concatenated with the + operator demonstrating Eiffel's ability to use operators not just for numerical types. Second, we convert the floating point number x to a string using the to_string feature. Eiffel uses, like many other object-oriented languages, the dot notation to refer to features of objects. For methods without parameters, we can omit the empty parameter list so that the call looks just like the access to an attribute. Using parentheses in this case will result in a compiler warning.
The statements are not ended or separated by any special character. You only need to use a semicolon if you try and put multiple statements in a single line. Here is the packed version of the program above.
class ARITHMETIC_PACKED creation make feature make is local i: INTEGER; x: DOUBLE do i := 50; x := 1.5 + 3 * 2.0^3 + i print("x=" + x.to_string + "%N") end end
Eiffel also has a boolean type and supports the usual boolean operators (using their proper names, not C's symbols).
class ARITHMETIC creation make feature make is local x: DOUBLE b: BOOLEAN do x := 100 b := x > 10 or x /= 50 and not ("blub" <= "blah") print("b=" + b.to_string + "%N") end end
Here, /= is obviously not the devide-and-update operator used in the C family, but the unequal sign. Also note that the parentheses around the string comparison are required, because the not binds stronger than the comparison operators.
Next to arithmetic, we have usually covered functions, which, in a purely object-oriented language such as Eiffel, means a method returning a value.
class FUNCTION_EXAMPLE creation make feature make is do print("result=" + times_square(2, 3).to_string + "%N") end times_square(x: DOUBLE; i: INTEGER): DOUBLE is do Result := i * x Result := Result * Result end end
Parameter and return types are specified like the types of local variables. The semicolon separating the two arguments is only needed if they are defined on the same line. The return value is defined using the implicit variable Result. As you can see, it can be used like any other variable. When the function is left, the value of this variable is returned to the calling routine.
Do not try to assign a value to a formal parameter of a method. In contrast to the C family, Eiffel does not allow this (mostly confusing) practice.
Eiffel restricts itself to a relatively small set of control statements, the usual if-then-else, a case statement, and a loop instruction that corresponds semantically to C's for loop. In all three cases, the syntax is straight forward.
class IF_EXAMPLE creation make feature make is do compare(4, 5) end compare(x: INTEGER; y: INTEGER) is do if x < y then print("less") elseif x = y then print("equal") else print("greater") end end end
The case statement is called inspect in Eiffel, but otherwise works as expected. The inspected expression (and thus all the expressions it is checked agains) must be an integer or a character.
class INSPECT_EXAMPLE creation make feature make is do print("2=" + to_string(2)) end to_string(x: INTEGER) : STRING is do inspect x when 1 then Result := "one" when 2 then Result := "two" when 3 then Result := "three" else Result := "another" end end end
The loop instruction can be viewed as a readable version of C's for statement. You define initialization instructions, an exit condition, and the body of the loop which is executed until the exit condition becomes true. In Section 14.2.4> we will cover the possibility to add invariants and variants to loops as part of the Design by Contract.
class LOOP_EXAMPLE creation make feature make is local i: INTEGER do from i := 1 until i > 3 loop print("i=" + i.to_string + "%N") i := i + 1 end end end
We had to define classes from the very beginning (even for our "Hello World" program), but for now we have used them merely to set the context for functions doing the rest. Let us now define our standard class example, a person, in Eiffel.
class PERSON creation make feature make(a_name: STRING, an_age: INTEGER) is do name := a_name age := an_age end to_string: STRING is do Result := "name=" + name + ", age=" + age.to_string end; feature name: STRING age: INTEGER end
The only new element are the two attributes name and age. They can be used inside the class like any other variable. Since we have not restricted the access to these features, they are also readable from any other class. However, you can not set an attribute from the outside, since this could cause an inconsistent state of the object. Eiffel does not know the concept of public read-write attributes (which is not good style in the languages supporting it). If you want other classes to be able to change an attribute, you have to define a setter method for it.
set_name(a_name: STRING) is do name := a_name end
Also note that Eiffel does not allow us to use the same name for two different features of a class even if they have different signatures. In Eiffel, a feature name is always unique. If we would like two different constructor methods, we have to give them different names.
Having defined the PERSON class, we would like to create person objects. Using Eiffel, objects spring into life with a bang (actually two).
class TEST creation make feature make is local person: PERSON; do !!person.make("Homer", 55) print("person=" + person.to_string + "%N") print("name=" + person.name + "%N") end end
If you prefer a more readable syntax, you can also use the keyword create instead.
create person.make("Homer", 55)
The variable person is a reference to an object of the class PERSON. At the beginning of the method, this reference is set to Void. We can easily verify this in the code using an assertion:
check person = Void end
The predefined constant Void corresponds to a null pointer just like Pascal's nil or Java's null. In contrast to most other object-oriented languages, we do not create an object (for example, using some factory method such a new) and assign it to a variable. Instead, Eiffel performs both in one step with the create or "bang bang" instruction applied to the variable.
Next, let's again derive an employee class that adds an employee number to a person.
class EMPLOYEE inherit PERSON rename make as person_make redefine to_string end creation make feature make(a_name: STRING; an_age, a_number: INTEGER) is do person_make(a_name, an_age) number := a_number end to_string: STRING is do Result := Precursor + ", number=" + number.to_string end; feature number: INTEGER end
The first striking element is the extensive declaration of the inheritance in the inherit clause. Since we would like to define a new constructor taking the employee number as an addition argument, we have to hide the original make feature of the PERSON class by renaming it to person_make (remember that feature names must be unique).
We would also like to provide a new implementation of the to_string method so that the employee number gets printed as well. To avoid simple mistakes such as an incorrect spelling of the redefined method, we must state our intention explicitly using the redefine instruction.
Once we have prepared the class in this manner, the implementation is straight-forward. In the new constructor make we can call the old one using its new name person_make. Similarly, the special name Precursor refers to the implementation of the current method in the parent class. This is used in the redefinition of the to_string method to add the employee number to the string provided by the PERSON class.
All strongly typed object-oriented languages let us declare abstract methods, that is, methods which rely on subclasses to provide an implementation. In Eiffel, we do not talk about abstract methods, but deferred features. Here is an example defining the interface for an account with the minimal balance, deposit, withdraw functionality.
deferred class ACCOUNT feature balance: DOUBLE is deferred end deposit(amount: DOUBLE) is deferred end withdraw(amount: DOUBLE) is deferred end end
Replacing the do block by the keyword deferred makes the features deferred. A class with at least one deferred feature is a deferred class and has to be marked as such.
Features without arguments can be implemented as methods or as attributes (that's why the more general term "deferred feature" makes sense). Here is probably the simplest implementation of the account interface.
class SIMPLE_ACCOUNT inherit ACCOUNT redefine balance, deposit, withdraw end feature balance: DOUBLE deposit(amount: DOUBLE) is do balance := balance + amount end withdraw(amount: DOUBLE) is do balance := balance - amount end end
Implementing a deferred class works just like inheriting from any other class. In the redefine clause, we tell the compiler which features we are going to implement. In this implementation of the account, the balance feature is implemented as an attribute.
Of course, deferred classes can not be instantiated. But now that we have an implementation, we can use the class in a test program.
class TEST creation make feature make is local account: ACCOUNT do !SIMPLE_ACCOUNT!account account.deposit(10.0) account.withdraw(5.0) print("balance=" + account.balance.to_string + "%N") end end
If you did not like the "bang bang" syntax for object creation, you won't like special case for derived classes either. The concrete class to be instantiated is put between the two quotation marks of the object creation instruction.
As mentioned in the introduction, Design by Contract sets Eiffel apart from other languages. When we look at a library, we want to know how to call a function and what a function does. The first question is answered by the function's signature. It tells us which parameters the function expects, and, in strongly typed languages, which type the supplied arguments must have. The semantics of the function, however, are normally described in comments only.
Eiffel goes one step further by giving us the means to specify some semantic information in the code. We can define semantic conditions on the input (preconditions), output (postconditions), and state of the object (invariants). Here is an example:
class ACCOUNT create make feature make(a_minimal_balance: DOUBLE; initial_balance: DOUBLE) is require consistent_balance: a_minimal_balance <= initial_balance do minimal_balance := a_minimal_balance balance := initial_balance ensure balance_set: balance = initial_balance minimal_balance_set: minimal_balance = a_minimal_balance end deposit(amount: DOUBLE) is require positive_amount: amount > 0 do balance := balance + amount ensure balance_updated: balance = old balance + amount end withdraw(amount: DOUBLE) is require positive_amount: amount > 0 enough_money: balance - amount >= minimal_balance do balance := balance - amount ensure balance_updated: balance = old balance - amount end feature -- attributes minimal_balance: DOUBLE balance: DOUBLE invariant balance_ok: balance >= minimal_balance end
The example defines an account with a constructor and the two methods deposit and withdraw. Here is a test program using the account class.
class TEST create make feature make is local account: ACCOUNT do !!account.make(-1000, 0) account.deposit(50) account.withdraw(150) print("balance=" + account.balance.to_string + "%N") end end
The interesting part is obviously not the minimal implementation of the method, but way the class and its method are adorned with conditions which ensure that the class works as expected. The constructor takes two arguments, a minimal and an initial balance. These arguments only make sense if the initial balance is not less than the minimal balance. Hence, we define a precondition in the require section of the method which checks exactly that. The purpose of the constructor is to set the attributes to the given values. This result expected by the client calling the method is verified using a postcondition in the ensure section of the method. Similarly, we define pre- and postconditions for the two other methods. The precondition makes sure that we never get below the minimal balance, and the postcondition check that the balance has been updated correctly. The nice syntactical old feature lets us refer to the value of the balance before the method is executed.
Finally, there is the invariant section of the class which lets us define conditions which have to be fulfilled by an object of the class at any time. In our case, we make sure that the balance never gets below the minimal balance.
These three elements, preconditions, postconditions, and invariants are at the heart of Eiffel's design by contract. I hope that even this simple example gives you an idea how much semantic information can be captured with these language constructs.
Eiffel also lets us add additional checks to the program flow. The simplest one is the check instruction which can be placed anywhere to check a condition (like C's assert).
class CHECK_EXAMPLE creation make feature make is local n: INTEGER do n := 5 check is_five: n = 5 is_positive: n > 0 end print("checks succeeded") end end
As already mentioned in Section 14.2.2>, it is also possible to add special checks to loops which help to prevent common errors such as infinite loops. Here is a program computing the greatest common divisor of two integers using Euclid's algorithm.
class GCD creation make feature make is do print("gcd(25, 35)=" + gcd(25, 35).to_string + "%N") end gcd(a, b: INTEGER): INTEGER is require a > 0 b > 0 local x, y: INTEGER do from x := a y := b invariant x > 0 y > 0 variant x.max(y) until x = y loop if x > y then x := x - y else y := y - x end end Result := x end end
A loop invariant is a condition which must be true during the whole iteration. In the example, the two variables x and y must stay positive. The variant of a loop is an integer expression which is always positive and becomes smaller from iteration to iteration. This way we can guarantee that the loop will end. In the Euclidian algorithm, we know that the maximum of x and y is a good candidate for a loop variant.
You may raise at least two questions at this point: What happens if one of the conditions is violated and what is the performance impact of all these checks? To answer the first question, let's try to create an account with an invalid balance by calling !!account.make(100, 10).
*** Error at Run Time ***: Require Assertion Violated. *** Error at Run Time ***: consistent_balance 3 frames in current stack. ===== Bottom of run-time stack ===== System root. Current = TEST#0x8061a60 line 4 column 2 file ./test.e ====================================== make TEST Current = TEST#0x8061a60 account = Void line 8 column 4 file ./test.e ====================================== make ACCOUNT Current = ACCOUNT#0x8061a88 [ minimal_balance = 0.000000 balance = 0.000000 ] a_minimal_balance = 100.000000 initial_balance = 10.000000 line 7 column 42 file ./account.e ===== Top of run-time stack ===== *** Error at Run Time ***: Require Assertion Violated. *** Error at Run Time ***: consistent_balance
That's what I call a comprehensive error description. Not only do we get the name of the violated condition and the values of the parameters passed to the constructor method, but also the state of the account object in question. Here is an example for the violation of a loop variant. Assume that we forget the update of the loop variable.
class LOOP_EXAMPLE creation make feature make is local i, n: INTEGER do n := 3 from i := 1 invariant i > 0 variant n - i until i > n loop print("i=" + i.to_string + "%N") end end end
Running this program results in the following error message.
i=1 *** Error at Run Time ***: Bad loop variant. Loop body counter = 1 (done) Previous Variant = 2 New Variant = 2 2 frames in current stack. ===== Bottom of run-time stack ===== System root. Current = LOOP_EXAMPLE#0x8061ab8 line 4 column 4 file ./loop_example.e ====================================== make LOOP_EXAMPLE Current = LOOP_EXAMPLE#0x8061ab8 i = 1 n = 3 line 14 column 15 file ./loop_example.e ===== Top of run-time stack ===== *** Error at Run Time ***: Bad loop variant. Loop body counter = 1 (done) Previous Variant = 2 New Variant = 2
Again, the error message points precisely at the problem.
How do all these assertion impact the performance? Eiffel allows to switch the checks on or off without changing the source code. This way, we can decide on a case by case basis whether the performance hit for the evaluation of the conditions is justified or not.
All features we have defined for now are public, that is, they can be accessed by any other class. However, Eiffel allows us to restrict the visibility of features. Eiffel does not use fixed visibility modifiers (e.g., private, protected, public). Instead, we can specify which classes are allowed to see a group of features. Together with the special classes ANY and NONE, we can express private, protected and public visibility, but have more freedom to give other classes access as well. Here is a simple example.
class COUNTER feature {ANY} increment: INTEGER is do count := count + 1 Result := count end feature {COUNTER} reset is do count := 0 end feature {NONE} count: INTEGER end
To restrict the visibility of a feature block, we add the names of the classes which are allowed to see the features in braces. The visibility always includes all subclasses of the specified classes. Since all application classes derive from ANY, the first feature increment is public. The reset feature is visible by the class COUNTER itself and all its children (protected feature in other languages). Finally the last feature count is restricted to the class NONE. As the name suggests, no other class can be derived from this special class, which makes the feature private.
By default, features are public. The feature definitions in the previous sections are just shortcuts for feature {ANY}. Besides the basic visibility rules demontrates above, we can also selectively give other classes access to certain features (similar to the friend mechanism in C++).