A comprehensive guide to Eiffel syntax

Introduction

Welcome to this guide about the Eiffel programming language. It is designed to be a basic reference to help beginners get acquainted with the language. Some knowledge of other programming languages might be considered very useful. This should not be seen as a course or tutorial. Please help improve and maintain this page through GitHub. To get started, let's have a look at this simple hello world:

Some basics

The first difference you will notice in Eiffel compared to other languages like C++ or Java is the absence of curly brackets to separate code blocks. Instead in Eiffel we use keywords followed by end. We also do not ordinarily terminate lines of code with a semi-colon. However, it is acceptable to separate statements with a semi-colon if you so wish. Features (functions) are called without using brackets, if they do not accept any arguments (e.g. target.my_function instead of target.my_function()).

Naming_conventions

In general, in Eiffel we use snake_script and not CamelCase.

CLASSES should be spelled in capital letters.

features (methods and attributes of classes) should be spelled small.

Objects (instances of a class) should be capitalised.

Features

In Eiffel, the Methods and Attributes (Variables) of a class are called features. We can define them by using the keyword feature followed by the features name within a class. We specify the features type or return type by using a colon followed by the types name. If a feature is a function and takes arguments we can list them in brackets together with their types. The keyword do is used to denote where the code of the function starts, followed by end at the end.

So, a client of the class would not be able to change the age by using Person.age := 21, as Person.age is read-only for clients. Instead it would call Person.set_age(21). However, if for some reasons you are feeling radical and want to use Person.age := 21, you can give age a so called assigner by using the assign keyword and specifying a setter.

Once-executed features

Although quite rarely used, in Eiffel a feature can be specified to only be executed once then save its return value and simply return it immediately at every subsequent call. To do this, simply use the once keyword instead of do.

This can be useful for initialisation. A setup feature could implement the initialisation of an object but be called from multiple other features. By using once, we can guarantee that it will only perform the initialisation once regardless of who calls it and how many times it is called.

It is also possible to specify in which context the feature should be considered as called. This is particularly important when using multiple threads or processes. For instance, you may want to execute a feature only once for each Process, once for each Thread or for every object instance. To specify this, use the once keys "PROCESS", "THREAD" and "OBJECT". These are specified as strings and in brackets after the once keyword like so: once ("PROCESS"). The default once key is "THREAD".

Classes

The simplest way to define a class in Eiffel is just to name it and give it some features. It may even inherit features from other classes. For this we can list all of the desired classes after stating the inherit keyword. Using the redefine keyword features may be specified for classes that should be redefined in this class and not inherited.

When redefining a feature, it can be helpful to call its old version. So, within the redefinition, the keyword Precursor is set to the old version and calls can be made like this: Precursor("Some argument"). If you would like to use a version from a specific parent, you can add curly brackets: Precursor { SOME_PARENT } ("Some argument")

In the case of MY_CLASS, when we want to create an instance of the class, we first need to define the object and specify its type. Then, we use the create keyword to initialise the object, before making other calls to it. A client of the class might look like this:

feature
example
local
New_object: MY_CLASS
do
create New_object
New_object.some_feature
end

Reference vs. Expanded Classes

In Eiffel there is a key differentiation between reference and expanded classes. By default a class is of the reference type. To declare a class as expanded, we use the expanded keyword before class.

As the name would suggest a reference class sets itself apart in that its attributes are references to other objects, either of classes defined by the developer or built-in classes such as STRING or REAL. As such, an object of a the reference type does not contain any actual values apart from addresses. It only contains references to where the values are stored in the memory. In C or C++ references would be referred to as pointers. An expanded class on the other hand does not contain references, but the actual values. This key difference has an effect on how we create and use classes.

Another difference, is that when used as an argument, data of an expanded type is passed by value, while data of reference types are passed by reference, since the object consists of addresses.

Reference Classes

When we define an object of a certain class, the computer will allocate memory to hold that class's attributes. However, in the case of a reference class, this allocated memory will only hold the references or addresses to the objects containing the actual values. So by default all attributes of a class will be set to Void, as the objects that the class refers to, do not exist yet. To create these we must always call create before using a new instance.

It is possible to test if an object x has been initialised yet by using the expression x = Void.

It is good style to use constructors to initialise attributes to the correct values and ensure that any class invariants are fulfilled. To enforce the use of constructors, we can specify features as possible constructors using the create keyword like so:

You may specify as many constructors as you want. However, as soon as at least one constructor has been specified, a constructor must be used when creating a new instance. If no constructors are specified, then simply creating an object without adding a constructor will invoke default_create, which is a feature that is automatically added to every class without constructors and by default does nothing.

Expanded Classes

Expanded classes differ from reference classes in that they do not contain references, but rather the actual values of their attributes. For this reason, we do not need to call create before using objects that are instances of the class. All attributes are automatically set to their default initial values when the object is defined.

If a and b are both instances of an expanded class, a := b will copy all of b (including its values) into a and create a new instance with the same values.

On the other hand, if a and b are both instances of a reference class, then a := b will copy the reference to the instance of the class represented by b into a. In other words, now a and b will reference the same instance and any change to a will be reflected in b.

Exporting features

By default all features defined in a class will be available to clients of the class. To prevent this, we can use {NONE} to keep features internal and inaccessible to clients. The Current keyword is considered a client. Hence, when using {NONE} for a feature, it will not be accessible using Current. This is similar to using private in Java for instance.

In fact, in Eiffel we can be very specific about which features are available to which clients, by specifying the class a client must have in order to access the feature. For this we once again use the curly brackets and list all the desired classes like so:

class
A
feature -- `s` will be available to all clients of the class.
s
...
feature {NONE} -- `u` and `v` will only be available internally.
u, v
...
feature {A, B} -- `x` will only be available to clients of the same type
x -- and to clients of the type `B`.
...
feature {C} -- `y` and `z` will be available only to clients of the type C.
y, z
...
end

One more thing to consider is, that creation procedures are not considered qualified calls. Therefore, when using a feature as a constructor, where it is exported to does not apply. if you would like to still specify which features are available to which classes, you can use the same notation, but with the create declaration.

Deferred Classes

Above we had a look at how classes can inherit and export features. Deferred classes have the capability to specify features without defining them, so that children of the class must themselves define them.

As soon as a class contains at least one deferred feature, it must be declared as deferred (notice the deferred keyword before class). A feature can be declared as deferred by using the deferred keyword, followed immediately by the end statement. A deferred feature does not have to be declared as redefined in a child class.

Aliases

When using a custom class to store data, it can be useful to use operators in order to compare objects of that class. For this we must define a feature that performs the comparison and is an alias for an operator. We use the alias keyword after the feature name to choose the operator. In this example, people will be compared according to their age:

If we now had an instance of the class called Joe with the age 36 and another called Tom with the age 24, then Joe > Tom is true, while Tom > Joe is false.

Multiple Inheritance

We have already seen, that classes can inherit features from other classes and redefine them to change their behaviour. We have also seen that classes may defer features to be implemented in a child class. However, when inheriting from multiple classes we run into another problem: clashes.

If a class C inherits from two classes A and B, which both have a feature called f then we will need to rename or undefine the feature for at least one of the two classes. For this we can use the rename and undefine keywords similarly to redefine. Consider the following implementation of the before mentioned problem:

class
A
feature
f
do
-- Some code...
end
g
do
-- Some code...
end
end

class
B
feature
f
do
-- Some different code...
end
g
do
-- Some different code...
end
end

class
C
inherit
A
rename
f as A_f -- The feature f inherited from A is now called A_f within C
end
B
undefine
g -- The feature g inherited from B is no longer part of C
end
feature
...
end

In the above example, the feature f inherited from B is still called f in C, the same goes for the feature g inherited from A. So in conclusion, the class C now has the features A_f (inherited from A), f (inherited from B) and g (inherited from A).

If the classes A and B were inheriting the features f and g from the same class instead of defining them themselves, then in fact there is no need to undefine or rename any of the features, as there is only one effective implementation for them.

Some basic I/O

These are some examples of basic input/output functions, that can be used to interact with a user at a command line level:

Io.put_string ("Hello World!") Prints out a string

Io.put_integer (42) Prints out an integer

Io.put_real (9.99) Prints out a real

Io.put_boolean (true) Prints out a true or false

Io.new_line Prints out a new line

Io.read_line Reads in one line of users input and stores it in Io.last_string

Io.read_integer Reads in an integer from users input and stores it in Io.last_integer

Current Though not an operator, this always references the currently executing instance of a class.

|..| Describes an integer interval. Useful in loops. e.g. 1 |..| 5

.. Describes an interval of integers or characters in inspect constructions. e.g. a .. z

and, or, xor, not Logic operators.

and then, or else, implies Semistrict logic operators (evaluation stops when the result is known).

Control Structures

In Eiffel the syntax for an if else structure is as follows (notice, there are no brackets):

if meaning_of_life = 42
then
-- code if true
else
-- code if false
end

Eiffel also provides switch-like Statements called inspect, where a variable is compared to various values. The else condition is the default condition that applies when no case matches the input.

inspect input_integer
when 2 then
-- Code when input_integer equals 2
when 3, 5 then
-- Code when input_integer equals 3 or 5
when 7..9 then
-- Code when input_integer equals 7 or 8 or 9
else
-- Code when input_integer does not equal 2, 3, 5, 7, 8 nor 9
end

Unlike switch statements in other languages, in Eiffel the code following a matching case is not evaluated and there is no break statement in Eiffel.

Loops

This is the typical syntax for a simplified from loop (comparable to for loops in other languages):

from
i := 0
until
i >= 10
loop
-- do something
i := i + 1
end

Notice, that in Eiffel loops are evaluated until the conditional becomes true rather than while the conditional is true, which is common in most other languages (Ex. for-loop in C, Java, etc.).

It is also possible to add contracts to a from loop. The two options here are to specify a variant expression and an invariant expression. The variant must decrease by at least 1 after each cycle of the loop, while the invariant remains the same. Here is an example:

The contracts in loops are designed to prevent bugs such as endless loops. As such the variant is supposed to be an estimation of the number of iterations.

There also exists an "across"-loop, which goes through an iterable object (such as a list), and creates a cursor. Make sure that the object is in fact iterable. For this all elements must have a feature called next and the iterated object should have the features first and last. The cursor points to the next element of the iterated object at each execution of the loop. Since it is a cursor, you must access the actual elements by using my_cursor.item.

across list_of_customers as customer loop
Io.put_string (customer.item.name)
Io.new_line
end

For instance, an integer interval is an iterable object.

across 1 |..| 5 as it loop
Io.put_integer (it.item)
Io.new_line
end

Contracts

Contracts are a concept used in Eiffel to avoid bugs. Although these should be disabled in a production runtime, during development they can be quite useful. There are three types of assurance elements: preconditions (used in features), postconditions (used in features) and class invariants.

Preconditions are defined using the require keyword. They should contain a tag and a boolean expression. Postconditions are written the same way, but we use the ensure keyword. We can use the old notation to compare a variable's value to its value before the feature was executed. Here is an example that might be used in the BAKERY class we saw above:

Class invariants are checked every time an operation is performed on the class, such as calling a feature. We declare class invariants using the invariant keyword. In this example we will check that a variable is always positive:

Genericity

This is a concept that is particularly useful when creating structures like lists. Using genericity we can define classes with generic types that can be specified later. Like this we can use the same class to make a list of strings and to make a list of integers for instance.

To use this in a class, the we can specify a generic parameter (ex. G) in in square brackets after the class name like so class MY_CLASS [G]. Then when defining an object of this class er must specify which class to use for the generic type like this: my_object: MY_CLASS[ STRING ]. This is a more detailed example of a generic class:

Agents

Particularly in event-driven programming, it can be useful to represent a feature using an object. For this we can use agents. We create an agent by using the agent keyword followed by the features we want to pass. To call the feature encapsulated by an agent object we use my_agent.call() if we expect no return value. To receive a return value we use my_agent.item() instead.

The following example is similar to a situation that might occur when programming a GUI. When run, the application prints "The button was clicked!".

class
APPLICATION
create
run
feature
run
local
button : BUTTON
do
create button.set_click_handler( agent click_event )
button.click
end
click_event
do
Io.put_string ("The button was clicked!")
end
end

To pass arguments to the feature when calling it through an agent we pass a tuple (denoted by square brackets) with all the arguments when invoking the agent. For this to work, we must also change the creation of the agent. To pass three arguments, we would create the agent using my_agent := agent my_feature(?, ?, ?) and then call it with a.call([argument_1, argument_2, argument_3]).

The type of an my_agent from above would be PROCEDURE[ T, TUPLE[ ARG1, ARG2, ARG3 ] ], where T is the class my_feature belongs to and ARG1, ARG2 and ARG3 are the types of argument_1, argument_2 and argument_3 respectively.

If the encapsulated feature is to return a value, then the type is actually FUNCTION[ T, TUPLE[ ARG1, ARG2, ARG3 ], RETURN_TYPE ].

Garbage collection

Eiffel uses a garbage collector which automatically removes any objects from the memory when they are no longer referenced anywhere. So unlike in C or C++ for instance, it is not necessary to manage the memory.