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:


class
    HELLO_WORLD

create
    say_it

feature
    say_it
        do
            Io.put_string ("Hello World!")
        end

end
    
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.



class
    BAKERY

feature
    
    number_of_cakes : INTEGER
        -- A variable containing an integer

    name_of_my_favourite_cake : STRING
        -- A variable containing a string

    price_of_one_cake : REAL
        -- A variable containing a floating point number

    buy_cake (price : REAL; flavour : STRING)   -- A function accepting arguments
        do
            -- Some code here...
        end

    is_cake_available : BOOLEAN   -- A function returning true or false 
        do
            Result := number_of_cakes > 0
        end

end

    

The return value of a feature is set by assigning it to the Result variable. Unlike other languages, the return statement does not exist.

Also notice, that when defining a feature, arguments are separated using semi-colons. However, when calling the feature, they are separated by means of a comma.

Features may also use local variables which are internal to the function. However, they must be declared with their type before the do keyword using the local keyword. For example:



class
    MY_CLASS
feature
    my_feature
        local
            my_variable_1 : INTEGER
            my_variable_2 : STRING
        do
            -- Some code here...
        end
end

    

Assigners

The variables of a class are read-only to its clients. To change their values, we can use a setter, a feature that takes the new value as an argument, like in the case of set_age below:



class
    PERSON
feature
    age: INTEGER
    set_age ( new_age: INTEGER )
        do
            age := new_age
        end
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.



class
    PERSON
feature
    age: INTEGER assign set_age
    set_age ( new_age: INTEGER )
        do
            age := new_age
        end
end

    

Now, Person.age := 21 is simply a shortcut for Person.set_age(21).

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.



feature
    first_name: STRING
    last_name: STRING
    full_name: STRING
        once
            first_name := "John"
            last_name := "Doe"
            Result := first_name + " " + last_name
        end

    

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.



class
    MY_CLASS

inherit
    SOME_OTHER_CLASS
        redefine
            some_inherited_feature
        end

feature
    some_feature
        do
            [...]
        end
    some_inherited_feature

[...]

end

    

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:



class
    MY_CLASS
create
    my_feature
feature
    my_feature (some_argument : STRING)
        do
            -- Do something here...
        end
end

    

An instance my_object of the class would then be initialised like so:



create my_object.my_feature("Hello World!")

    

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.



deferred class
    MY_CLASS

feature
    some_feature: STRING
    another_feature
        deferred
        end

end

    

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:



class
    PERSON

feature
    name: STRING
    age: INTEGER

    older_than alias ">" (other: PERSON) : BOOLEAN
        do
            Result := (age > other.age)
        end

end

    

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

Operators

:= Assignment operator. Ex: meaning_of_life := 42

= Equality operator (== in many other languages). Ex: 1 + 2 = 3 would be true

/= Inequality operator (!= in many other languages). Ex: meaning_of_life /= 42

<, >, <=, >= Comparison operators.

+, -, *, / Mathematical operators.

// Integer division operator. Ex: 5/2=2.5 vs 5//2=2

\\ Modulo. Ex: 5\\2=1

equal (x, y) To compare strings we can use the equal function.

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:



from
    i := 0
    n := 10
invariant
    n > 0
until
    i >= 10
loop
    i := i + 1
variant
    n-i
end

    

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:



number_of_available_cakes : INTEGER

buy_cakes (amount : INTEGER)

    require
        positive_amount: amount > 0    -- Check that amount is a positive number

    do
        number_of_available_cakes = number_of_available_cakes - amount

    ensure
        amount_reduced: number_of_available_cakes = old number_of_available_cakes - amount
            -- Check that the number of available cakes has decreased correctly

    end

    

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:



class
    MY_CLASS

feature
    some_number : INTEGER
    some_feature
        do
            -- Some code here...
        end

invariant
    positive: some_number > 0

end

    

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:



class MY_LIST [G] feature

    first : G
    last : G
    extend (new_element: G)
        do
            -- Add element to list...
        end

end

    


class SCHOOL feature

    list_of_students : MY_LIST[ STUDENT ]
    list_of_classes : MY_LIST[ MY_LIST[ STUDENT ] ]

end

    

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

    


class
    BUTTON
    
create
    set_click_handler
    
feature

    click_handler : PROCEDURE [APPLICATION, TUPLE[]]
    
    set_click_handler ( handler: PROCEDURE [APPLICATION, TUPLE[]] )
        do
            click_handler := handler
        end
        
    click
        do
            click_handler.call
        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.