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
endWe 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
endIn 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
endEiffel 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
endHere, /= 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
endParameter 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
endThe 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
endThe 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
endWe 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
endThe 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
endAlso 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
endIf 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
endThe 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
endReplacing 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
endImplementing 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
endIf 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
endThe 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
endThe 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
endAs 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
endA 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_balanceThat'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
endRunning 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
endTo 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++).