Copyright © Simon Parker, 1992, 1993,
1999
Version 3.2
An Eiffel program is known as a system, and its components are classes. There may be any number of classes in a system. Many will be general classes, of value in different contexts. Some will be classes written specially for the system, but a fundamental goal of Eiffel is to help build reusable software.
An Eiffel development system comes with libraries of general-purpose classes. Many basic classes are defined with the language, and must be present. Others may be provided with the compiler, or bought from independent suppliers. During the course of developing successive projects, an organisation accumulates classes which match the kind of application it deals with.
The full text of an example class ENTRY is reproduced in Figure
1.
class
ENTRY
-- One entrant's time in a race
feature
entrant: STRING
-- Entrant, identified by sailnumber
elapsed_seconds: INTEGER
-- Actual elapsed time, not adjusted
elapsed_minutes: REAL is
-- Actual elapsed time, not adjusted
-- in minutes and decimal minutes
do
Result := elapsed_seconds / 60
end -- elapsed_minutes
status: INTEGER
-- Status of entrant in this race
dnc: INTEGER is 0
-- Status value, 'Did not compete'
ok, rtd, dsq: INTEGER is unique
-- Status values:
-- Finished, Retired, Disqualified
finish (in_time: INTEGER) is
-- Entrant finished. Note elapsed time, in seconds
do
elapsed_seconds := in_time
status := ok
end -- finish
retire is
-- Entrant retired from race
do
status := rtd
end -- retire
disqualify is
-- Do not score this entry
do
status := dsq
end -- disqualify
started: BOOLEAN is
-- Did the entrant start the race?
do
Result := status = ok or status = rtd
end -- started
finished: BOOLEAN is
-- Did the entrant finish the race?
do
Result := status = ok
end -- finished
retired: BOOLEAN is
-- Did the entrant retire?
do
Result := status = rtd
end -- retired
disqualified: BOOLEAN is
-- Was the entrant disqualified?
do
Result := status = dsq
end -- disqualified
identify (sailnumber: STRING) is
-- Record or change the entrant
do
entrant := sailnumber
end -- identify
identity: STRING is
-- Readable form of entrant, even if none
do
Result := entrant
if entrant = Void then
Result := "(Unidentified)"
end
end -- identity
end -- class ENTRY
Figure 1
This nautical example is part of a system for scoring dinghy races. I have modified it for teaching purposes, but it is representative of the style of Eiffel programming and it compiles and executes on my machine. The class contains examples of everything discussed below. Look at it now, and refer to it as you read.
A simple class consists of a header, some features and an end delimiter. Eiffel syntax is not very demanding - much of the decoration is due to convention. Double hyphen introduces a comment, which lasts to the end of the line. Class headers and feature headers should be followed by a terse, useful comment. End delimiters should echo the corresponding headers. Class and type names are written in uppercase, most other words are in lowercase. All the punctuation here is mandatory. I invented the name of the class and the names of the features, but all the other words are reserved.
Features are either attributes or routines. Routines are either procedures or functions.
The first two features are both attributes. The first can hold an arbitrary
character string, the second an integer. Other basic types include
REAL and DOUBLE for floating point numbers, and
BOOLEAN for truth values.
The third feature, elapsed_minutes, is a function returning a
floating point number. The signature is similar to those of the attributes, but
it has a body introduced by the keyword 'do'. The similarity is deliberate, a
consequence of applying the principle of information hiding.
The value returned by a function is whatever value is in 'Result' when the
function ends. 'Result' is declared and initialised automatically at the start
of the function, and you may change its value as many times as you want. In this
case, the statement in the routine body assigns the value of a numeric
expression to Result. Note that the assignment operator is ':=',
and the equal sign alone always tests for equality.
status is an integer attribute. Like all numeric attributes, it
will be initialised automatically to zero by default. The four declarations
following status are also attributes, since they have no routine
bodies. They all have literal values, however, which makes them constant
attributes. The compiler will substitute the values for the names wherever
they are mentioned. The value of the attribute dnc is explicitly
stated as zero, matching the default initial value of status.
ok, rtd and dsq will be given arbitrary
sequential values by the compiler, guaranteed greater than zero and unique in
this class.
Routine finish takes an integer argument, as shown by the
parentheses. Because it has no return type it is a procedure rather than a
function. The two statements in its routine body may be separated by a
semi-colon if you want, but no punctuation is required at all. Procedures
retire and disqualify are also procedures, but take no
arguments. The next group of boolean functions provide the means to test the
status of this entry. Note the use of the operator 'or' in function
started; operator precedence rules ensure this expression is
interpreted as you would expect.
Before considering the last two features of this class, we need a little background on objects and types.
A named place where a value can be kept, typically in memory, is called an entity. We have encountered three of the four kinds of entity:
Result (automatically defined in each function)
Every entity has a static type. For attributes and arguments the type is part of the declaration. The type of Result is implied by the signature of the function in which it is defined.
An entity can only hold values of its own type. If you assign a string value
to an integer entity like elapsed_seconds at run time, the
assignment will fail. This is called type failure: in production software
it is disastrous.
Eiffel is a statically typed language. It has been designed in such a way that it is always possible for the compiler to verify that a type failure cannot happen.
The entities we have seen so far, types INTEGER,
REAL and BOOLEAN, are all big enough to contain the
corresponding simple numeric values. These types are called expanded
types.
The attribute entrant has the type STRING.
Character strings vary in length and are too complex to keep in an entity.
STRING objects are therefore stored elsewhere, and the entity
contains only a reference to the place where the object is stored.
STRING is an example of a reference type.
Expanded types and reference types behave differently in some situations. If an object of an expanded type is assigned to an entity,
elapsed_seconds := in_time
a fresh copy of the number is assigned to the attribute. Doing the same with a string:
entrant := sailnumber
just creates a new reference to the same string. You will find that most types are reference types, so it is worth getting used to this behaviour.
When an entity is created it is always initialised. For numbers, the initial
value is zero. Boolean entities are always set to False. All
entities of reference types are set to a special value called Void,
indicating they do not refer to any object. We say they are void
references, and the final function identity shows how to test
for this condition. It also contains an example of a string constant, and
contains two assignments to Result.
One of Eiffel's design goals is to support the large scale reuse of software. In this context there is an important distinction between the supplier of a class and the consumer.
Both roles are played by software designers, but with different perspectives. The supplier is concerned with encapsulating useful abstractions elegantly and efficiently. The consumer is concerned with selecting and combining classes to build higher level software with as little new code as possible.
As software is built in successive layers the Eiffel programmer usually has a
foot in both camps. These roles are played by the classes as much as by the
designers. The term
The Eiffel libraries illustrate this distinction. They contain a rich collection of software, all built for other people to use. Each compiler vendor provides libraries addressing fundamental programming needs like data structures, maths, input and output. With the first commercial Eiffel compiler, ISE bundled eight libraries containing hundreds of classes. To give you a feel for the scale of this software here is an arbitrary selection of class names.
How do clients use this software? The programmer learns how to use libraries from documentation, of which more later. Application code uses classes by creating objects and calling features. Most Eiffel programming consists of writing feature calls.
The Eiffel feature call uses a 'dot' notation. For example,
ff_start_time.adjust (0, 20)
Here, ff_start_time is an entity referring to a
TIME_OF_DAY object. The effect of the call is to advance the
minute value by 20. Figure 2 shows the full text of class
TIME_OF_DAY.
class
TIME_OF_DAY
-- Absolute time within a day to the nearest minute
feature
hour: INTEGER is
-- Hour of day, 00 to 23
do
Result := minutes // 60
end -- hour
minute: INTEGER is
-- Minute in hour, 00 to 59
do
Result := minutes \\ 60
end -- minute
set (hh: INTEGER, mm: INTEGER) is
-- A new time
require
0 <= hh and hh < 24
0 <= mm and mm < 60
do
minutes := hh * 60 + mm
ensure
hour = hh
minute = mm
end -- set
adjust (hh_by: INTEGER, mm_by: INTEGER) is
-- Advance (+) or retard (-)
-- either unit or both
do
minutes := minutes + hh_by * 60 + mm_by
normalise
end -- adjust
feature {NONE}
minutes: INTEGER
-- Minutes since midnight
normalise is
-- Restore invariant after adjustment
do
minutes := minutes \\ 1440
if minutes < 0 then
minutes := minutes + 1440
end
end -- normalise
invariant
0 <= hour and hour < 24
0 <= minute and minute < 60
0 <= minutes and minutes < 1440
end -- class TIME_OF_DAY
Figure 2
Actual arguments in the call must match the formal arguments in the signature in number, position and type. If the feature takes no arguments, there are no parentheses. For example,
hh := ff_start_time.hour
The designer of a class controls which clients may call its features.
Feature declarations are organised in groups according to their visibility. Each group has a header which lists the classes which may use the features which follow it. A client may only call a feature if the client belongs to one of the classes in the list.
If the header has no list, then any client may call the features. If the list
is empty, or contains the word NONE, then the features are for
internal use only.
For example, clients of TIME_OF_DAY may not use features
minutes and normalise because they are in an internal
group.
Inside the class all features are always visible. The value of an attribute may be changed by any feature in the class, but never by any client.
The body of a routine is the private domain of the implementor. No client should depend on the internal details, but only on the public interface to the routine as expressed by its signature.
A signature alone provides essential information about how to call a feature. The types of the arguments and return value will be checked by the compiler for each call.
Eiffel brings to practical commercial programming some important notions from the discipline of formal specification. Three clauses, all optional but strongly recommended, allow the supplier to specify rigorously much that is traditionally been left to hearsay, convention or 'Technical Documentation'.
The 'require' and 'ensure' clauses are associated with a routine, the 'invariant' clause with a class.
The require clause introduces a precondition for a routine. A precondition
states the circumstances under which the routine may be called. It may impose
restrictions on the values of arguments, as in procedure set of
TIME_OF_DAY. It may refer to the state of the object. For example,
in some class concerned with output you might find this:
write (text: STRING) is
require is_open
...
The ensure clause introduces a postcondition. A postcondition declares what
effect the feature will have. It may refer to the value returned by the feature
or to the state of the object after the call. The ensure clause for procedure
set says precisely what the procedure does, without waffle. More
signifantly, the clause can be executed.
The invariant clause states 'eternal truths' about the class. In theory,
these conditions must always be true. In practice the rules are suspended while
a routine is executing, but the routine must 'restore the invariant' before it
returns. This is what routine adjust of class
TIME_OF_DAY does by calling normalise.
You can think of preconditions and postconditions as a contract governing the use of a feature. The caller is responsible for the preconditions, and the feature is not obliged to work at all if they are not met.
On the other hand the feature is responsible for satisfying the postconditions and the caller can rely on them. Liability on each part is limited to the terms of this contract.
Because the require clause is executable, the routine need not repeat the checks. Because the clause is binding on the caller, the routine need not consider error handling. It only has to do the job it was designed for!
Similarly, the ensure clause allows the caller to proceed after the call confident that the work has been done. There is no need to check whether the call succeeded.
An automatic documentation tool called 'short' reinforces these principles by producing a version of an Eiffel class with all implementation details suppressed. It retains all the interface information, but only the interface information. The idea is that client programmers can work from the 'short' version of a class.
A short version of class TIME_OF_DAY is shown in figure 3.
Compare it carefully with the class text in figure 2.
class interface
TIME_OF_DAY
-- Absolute time within a day to the nearest minute
feature
hour: INTEGER
-- Hour of day, 00 to 23
minute: INTEGER
-- Minute in hour, 00 to 59
set (hh: INTEGER, mm: INTEGER)
-- A new time
require
0 <= hh and hh < 24
0 <= mm and mm < 60
ensure
hour = hh
minute = mm
adjust (hh_by: INTEGER, mm_by: INTEGER)
-- Advance (+) or retard (-)
-- either unit or both
invariant
0 <= hour and hour < 24
0 <= minute and minute < 60
end -- class interface TIME_OF_DAY
Figure 3
Assertions are part of the interface - both parties must see the contract! They therefore form a crucial part of the documentation, and are preserved in the short version of a class.
The Technical Documentation can devote itself to general discussion of how the classes and features are intended to be used. It might be brief and general enough to be read, and even maintained!
You create an object by executing a creation instruction. In its simplest form it is a pair of exclamation marks followed by the name of the target entity.
finish_time: TIME_OF_DAY;
...
!!finish_time
The effect of executing this instruction is:
TIME_OF_DAY
finish_time with a reference to the new
object TIME_OF_DAY objects have only one attribute (minutes) which is
initialised to zero since its type is INTEGER. This produces a
valid object; that is, it satisfies the invariant.
It is good Eiffel style to arrange that the default initialisation of each attribute will yield a valid object. In situations where this simple approach is inadequate, an alternative form of the creation instruction is used.
Consider class TIME_OF_DAY. The simple creation instruction
above creates an object with a value of midnight. Clients will probably follow
the creation instruction with a call to feature set:
lunchtime: TIME_OF_DAY
...
!!lunchtime
lunchtime.set (12, 30)
We can save our clients the burden of writing a separate call by nominating
procedure set (and possible others) as a creation procedure. Then
the procedure can be invoked by including the feature name and arguments in the
creation instruction:
lunchtime: TIME_OF_DAY
...
!!lunchtime.set (12, 30)
The effect of executing this instruction is:
TIME_OF_DAY
lunchtime with a reference to the new
object
set of the new object with arguments
(12, 30) The penalty our clients pay is that the simple form becomes illegal. The price the supplier pays is a new part in the class definition. The creation part comes before the first feature header, and the syntax is straightforward: the word 'creation' followed by a list of procedure names.
class TIME_OF_DAY
-- Absolute time within a day to the nearest minute
creation
set
feature
...
New objects can also be created from old ones using a 'clone' call. Clone looks like a function, returning a new object which is a copy of its argument. For example:
ff_start, ent_start: TIME_OF_DAY
...
!!ff_start
...
if starting_together then
ent_start := clone (ff_start)
end
One of the distinguishing aspects of object-oriented designs is that the intelligence is well distributed.
A system consists of a collection of co-operating classes, each designed to do a particular job thoroughly. The importance of the controlling routine, the 'main program' is diminished.
During execution, Eiffel instructions create objects and call features of those objects. You cannot write an Eiffel routine outside a class. The first routine executed is the creation procedure of the first object, which typically creates other objects and calls other routines. When the creation procedure ends, the system terminates.
The first object is known as the root, and its class is the root class. The root class for a system, and the creation procedure to be used, are nominated outside the code in a project description using some vendor-specified syntax.
There is nothing special about a root class. It is simply a high level component, which can be used by client programmers to build software. This means that one user's entire application might become just a menu option in another user's system. For example, consider the components implied by the names 'admission_dialogue', 'booking_session' and 'account_enquiry'.
This makes for reliable and extendable architectures, and offers a powerful approach to managing the complexity of big software systems.
Inheritance is one of the core techniques of object-oriented programming, and in Eiffel it is used freely for various purposes.
The next few examples are taken from a small set of classes (a 'cluster')
concerning time. The design of this cluster is based on the notion that units of
time sometimes behave like the digits in a number. For example, class
MIN12 represents the time of day to the nearest minute in the style
"12:34 AM", as a three-digit number. Some of the classes in the cluster are
listed in Figure 4, with inheritance relationships implied by indentation.
Figures 5, 6 and 7 show the full text of classes DIGIT,
DIGIT12 and MIN12 respectively. Fragments of other
classes appear in the text.
DIGIT -- Component of a number
DIGIT2 -- Binary digit
AMPM -- Component of time: morning or afternoon
DIGIT10 -- Decimal digit
DIGIT12 -- Digit, base 12
HOUR12 -- Hour of day, 12 then 1 to 11
MON12 -- Month of year, January to December
DIGIT24 -- Digit, base 24
DIGIT60 -- Digit, base 60
DIGITV -- Digit, radix can change dynamically
NUMBER -- Sequence of related digits
NUM10 -- Decimal number
HMAMPM -- Time of day, as in 2:34 PM
HMSAMPM -- Time of day, as in 2:34:56 PM
HMS24 -- Time of day, as in 14:34:56
Figure 4
Eiffel inheritance terminology has a genealogical flavour.
HOUR12 is an heir of DIGIT12, and
DIGIT12 is the parent of HOUR12. The
ancestors of a class include the class, its parent, its grandparent and
so on. The descendants include the class, all its heirs, all their heirs
and so on. Note that these terms are inclusive - a class is its own ancestor and
descendent.
To declare that class HOUR12 inherits from class
DIGIT12, you write an 'inherit' clause in class
HOUR12. Class DIGIT12 remains completely untouched.
class
HOUR12
-- Hour component of time of day
inherit
DIGIT12
end -- class HOUR12
This is a complete and valid Eiffel class declaration. It declares that
HOUR12 is an heir of DIGIT12, and is identical except
for the name.
Using inheritance to provide a more appropriate name for a class in some new situation is legitimate. This is not the only way to do it: your compilation system should also provide class name management facilities too.
In the same spirit, the client programmer may change the name of an inherited feature to something more suitable for clients.
If you were designing HOUR12 and decided that
advance and retard were more meaningful to your
clients than increment and decrement, you could
include a 'rename' phrase in the inherit clause.
class
HOUR12
-- Hour component of time of day
inherit
DIGIT12
rename
increment as advance,
decrement as retard
end
end -- class HOUR12
The designer of the heir class gets unimpeded access to all the features of all its ancestors, whether they are exported or not. The designer of a new class is encouraged to reuse as much as possible, including the implementation as well as the interface.
The heir class determines the visibility of the inherited features to clients. An inherited feature retains its export status unless it is explicitly mentioned in an 'export' clause.
Class DIGIT declares a hidden feature more_sig
holding a reference to the next digit. If you are designing an heir
DIGITF and want to make more_sig visible to class
NUMBER, you must write an export phrase in the inherit clause.
class
DIGITF
inherit
DIGIT
export
{NUMBER} more_sig
end
end -- class DIGITF
You may write as many 'export' phrases as necessary, and they can mention any classes and any inherited features. You may hide visible features as well as revealing hidden ones.
Inheritance is most commonly used for specialisation. The heir implements the behaviour defined by the parent in some specialised way.
The real difference between HOUR12 and DIGIT12 is
the way the values are presented. Feature symbol as inherited would
present the hours as "0" to "11". We need the first hour to be presented as "12"
instead, so we must provide a new implementation of symbol.
The new implementation is written in the 'features' part just like a new feature. The 'redefine' phrase in the inherit clause reassures the compiler that we know what we are doing.
class
HOUR12
-- Hour component of time of day
inherit
DIGIT12
redefine
symbol
end
feature
symbol: STRING is
-- 12, 1, 2... 10, 11
do
...
end -- symbol
end -- class HOUR12
It is very important not to abuse this mechanism to implement different behaviour, and Eiffel's pre- and postconditions have a significant role to play here. When a descendent redefines a routine, the inherited precondition and postcondition continue to govern the new definition. Any new assertions associated with the more specialised routines are combined with the inherited assertions in a manner which protects existing code. This is emphasised by the syntax: the preconditions and postconditions of a redefined routine are introduced by the words 'require else' and 'ensure then'.
Another reason for defining a new heir is to extend the capabilities of the parent by introducing new features.
There is no need to identify such features in the inherit clause, but if they are to be visible they must be exported. As with redefined routines, the new features must preserve the class invariant.
Class DIGITV needs extra features to vary the radix. Features
set_highest, set_lowest and set_range are
declared in the features part, but they are not mentioned in the inherit clause.
In any significant programming project, there are general-purpose routines and constants which are useful in many situations. Many of these are not naturally associated with any identifiable abstraction. In traditional environments such routines are gathered in libraries and declared in client source text by various means.
Inheritance provides the equivalent mechanism in Eiffel. Routines and constants are gathered in classes whose only purpose is to make them available through inheritance. In OOP parlance, such classes are sometimes known as mixin classes.
Class GENERAL in the kernel library provides a range of
facilities which are automatically inherited by all classes. This means that the
simplest of classes you define will have some innate features. Here are some of
the standard features; your vendor may also provide others, and you can add your
own.
is_equal, copy, clone deep_equal, deep_copy, deep_clone conforms_to maximum_character_code
Other basic classes supplied with the compiler will encapsulate mathematical and conversion routines. Input, output, exception handling and access to the internal representation of objects are also provided in this form.
Abstract classes allow the designer to model high-level concepts precisely without pre-empting physical design decisions.
Most abstract classes represent 'abstract data types', either general data
types such as STRING or ARRAY, or application data
types like PATIENT or HOSPITAL_STAY.
Other abstract classes encapsulate frameworks. A framework implements a general algorithm but leaves specific processing steps for specialised descendants to supply.
Some abstract classes simply gather together features which were common to two or more otherwise unrelated classes. Such classes might be produced in a generalisation stage near the end of a project.
There is little to distinguish an abstract class. The Eiffel inheritance mechanism and lenient syntactical requirements allow the designer to write legal Eiffel classes describing developing concepts at an early stage.
Two aspects of the language are particularly useful to the designer of abstract classes: deferred features and genericity.
Usually the abstract class can provide some reasonable default implementation
for descendants to redefine if necessary. Class DIGIT provides a
default implementation of symbol, for example.
Often this is either not feasible or not appropriate. In this case you need to make sure that descendants will define the missing feature - and do so properly. You do this by writing a deferred feature.
A class containing a deferred feature is called a deferred class. You may not create instances of a deferred class, only inherit from it. To make this obvious, the class header is flagged.
deferred class
DIGIT
A deferred feature has a name, comment and assertions like any other feature,
but the word 'deferred' takes the place of a body or value. In class
DIGIT, features lowest and highest are
both deferred routines.
These features are more than just placeholders, or forward references. Their
behaviour is specified by these signatures, and further constrained by
expressions in the invariant of class DIGIT (Figure 5).
deferred class
DIGIT
-- Component of a number
feature
make is
-- Default creation feature for convenience
do
end -- make
value: INTEGER
-- current value
symbol: STRING is
-- Printable representation
do
Result := value.out
end -- symbol
connect (left: DIGIT) is
-- Attach a more significant digit
do
more_sig := left
end -- connect
increment is
-- Add 1 to this digit
do
if value < highest then
value := value + 1
else
carry
value := lowest
end
ensure
value = old value + 1
or value = lowest
end -- increment
increment_by (how_much: INTEGER) is
-- Add how_much to value, carrying as necessary
require
positive: how_much >= 0
local
remains: INTEGER
do
from
remains := how_much
variant
remains
until
remains < 1
loop
increment
remains := remains - 1
end
end -- increment_by
decrement is
-- Reduce value by 1, borrowing if necessary
do
if value > lowest then
value := value - 1
else
borrow
value := highest
end
ensure
value = old value - 1
or value = highest
end -- decrement
decrement_by (how_much: INTEGER) is
-- Subtract how_much from value, borrowing as
-- necessary
require
positive: how_much >= 0
local
remains: INTEGER
do
from
remains := how_much
variant
remains
until
remains < 1
loop
decrement
remains := remains - 1
end
end -- decrement_by
feature {NONE}
lowest: INTEGER is
-- lowest legal value
deferred
end
highest: INTEGER is
-- highest legal value
deferred
end
more_sig: DIGIT
-- Next higher digit in number, if any
carry is
-- Overflowed this digit
do
if more_sig /= Void then
more_sig.increment
end
end -- carry
borrow is
-- Underflowed this digit
do
if more_sig /= Void then
more_sig.decrement
end
end -- borrow
invariant
range_positive: highest >= lowest
value_high_enough: lowest <= value
value_low_enough: value <= highest
end -- class DIGIT
Figure 5
Any descendent which supplies implementations for the deferred features (in Eiffel terms, 'effective implementations') must abide by the conditions laid down here.
Class DIGIT12 (in Figure 6) is an heir which provides effective
implementations for highest and lowest.
class
DIGIT12
-- Digit with base 12
inherit
DIGIT
creation
make,
connect
feature
lowest: INTEGER is once Result := 0 end
highest: INTEGER is once Result := 11 end
end -- class DIGIT12
Figure 6
Sometimes a supplier programmer designing an abstract class must deal with objects whose type only the client programmer will know. In this case the supplier offers a generic class, and the missing type information must be provided by parameter as part of an extended declaration. A generic class is not necessarily deferred; it may be used by clients without inheriting.
Suppose we are building a class representing a number, and want an array
which can hold DIGIT objects. The ARRAY class from the
basic libraries will serve our purpose. ARRAY is a generic class,
and we must provide the type of the array elements in brackets as part of the
declaration.
digits: ARRAY [DIGIT]
...
!!digits.make (1, 3)
The creation instruction creates an array with three elements, each of which
can contain a reference to a DIGIT object. An attempt to store any
other type, such as a STRING, will be rejected by the compiler as a
type violation. On the other hand, any object obtained from this array is
guaranteed to be a DIGIT object.
Class ARRAY is not called upon to do more than store and
retrieve elements, and client programmers are free to use it for objects of any
type. More specialised generic classes need the co-operation of the objects they
manipulate, so more control is needed.
The supplier includes the name of a class, called a 'constraining class' in the interface of the generic class. The types which the client programmer may specify in the declaration are then restricted to descendants of the constraining class.
Suppose we are designing a new class SORTED_LIST. Somewhere in
the text of the class will be an expression comparing elements. This can only
work if the elements are of a type for which comparison is meaningful.
The behaviour of the relational operators ( <, >, =, ...) is specified
in a small abstract class in the basic libraries called COMPARABLE.
Other basic classes inherit from it, including STRING.
We can ensure that only descendants of COMPARABLE will be
admitted to SORTED_LIST objects by making it the constraining
class.
class
SORTED_LIST [ET -> COMPARABLE]
Whenever we need to refer to the element type in class
SORTED_LIST, we write ET. The signature of the
function returning the first element would be written:
first: ET
The code inserting a new element might contain code like this:
this_element, that_element: ET
...
if this_element > that_element then ...
One of the fundamental principles of Eiffel is that a class is both a module and a type. Inheritance defines a type hierarchy, in which an heir defines a subtype of the parent.
A related concept is conformance; broadly, one type conforms to another if its corresponding class is a descendent of the other's. In any situation where an object of a certain type is required, an object of any conforming type can always be used.
This gives rise to the distinction between the static and dynamic types of an entity. The static type is the type written in the class text. The dynamic type is the type of the object it actually refers to at run time.
A programmer's reaction to the temptation to test and switch on the dynamic type of an object is a test of commitment to the object-oriented paradigm!
The attribute more_sig in class DIGIT (Figure 5)
holds a reference to the next digit in a number. This is to support procedure
carry, which must increment it from time to time. The static type
of more_sig is DIGIT.
Class MIN12 (Figure 7) combines three different subtypes of
digit to show hour, minute and AM or PM. Its creation procedure creates hh, mm
and ampm and connects them together.
class
MIN12
-- Time in 24 hours, to nearest minute, in 12 hour format
inherit
NUMBER
redefine
out
end
creation
make
feature
ampm: AMPM
hh: HOUR12
mm: DIGIT60
make is
-- Create and link digits
do
!!ampm.make
!!hh.connect (ampm)
!!mm.connect (hh)
end -- make
least_sig_digit: DIGIT is
once
Result := mm
end -- least_sig_digit
out: STRING is
-- 12:34 AM
do
Result := clone(hh.symbol)
Result.append_character (':')
Result.append_string (mm.symbol)
Result.append_character (' ')
Result.append_string (ampm.symbol)
end -- out
end -- class MIN12
Figure 7
After the connections are made the dynamic type of mm.more_sig
is HOUR12, and the dynamic type of hh.more_sig is
AMPM. Since both HOUR12 and AMPM conform to DIGIT,
this is legal.
When procedure mm.carry calls more_sig.increment,
the runtime system finds the version of increment appropriate for
HOUR12 objects.
When hh.carry executes the call more_sig.increment,
it is the version of increment in class AMPM which is invoked.
This dynamic binding mechanism is the source of much of the flexibility and power of object-oriented programming. In Eiffel it is supplemented by the security of strong typing. The type system ensures that there will always be at least one version of a feature available, and dynamic binding selects the right one.
Soon after you begin to think about object-oriented design you are likely to encounter a situation in which a new class wants to inherit behaviour from more than one parent. In many cases a review of the surrounding classes will reveal a simpler design involving only one parent, but multiple inheritance is often the most elegant and natural solution.
Eiffel does not discourage multiple inheritance; on the contrary, the technique is used widely.
Since the structuring unit in Eiffel is the class, and there are no global features, inheritance is the way to bring features of one class into the scope of another class.
It is common to package specialised behaviour in small classes which do not represent useful abstractions on their own. In order to augment the capabilities of your application classes with such 'mixins' without distorting the natural inheritance structure you need multiple inheritance.
Multiple inheritance is achieved by writing the names of all the parent classes in the 'inherit' clause. Each parent class name may be followed by 'rename' and 'redefine' phrases appropriate to that parent. The parents may be listed in any order, there may be any number of them, and they may be generic classes.
There is one major restriction: none of the parents may be a descendent of the class being defined. In other words, the 'inheritance hierarchy' may not contain cycles.
Parents might come from different lines of development, and even from different vendors. It is likely that the names of some of the inherited features will clash. The designer of the heir is obliged to resolve such clashes explicitly by renaming one or more of the inherited features.
Since multiple inheritance is permitted, it is also necessary to provide for repeated inheritance.
Direct repeated inheritance occurs when the same class is listed more than once in the inherit clause. This sounds an unlikely situation, but it is entirely legitimate and can be useful.
Indirect repeated inheritance means that two or more of the parents share a
common ancestor. Since all Eiffel classes trace their origins back to
ANY, multiple inheritance always introduces repeated inheritance.
As you might expect, Eiffel resolves the potential conflicts without fuss. Any feature which is inherited via more than one path, and ends up with the same name, becomes a single ('shared') feature in the heir. If it has been renamed along one path, the inherited features remain distinct, or 'replicated'.
In all other cases there is no obvious way to resolve the conflicts. Eiffel doesn't attempt to guess your intentions, so you need to make an explicit choice. There are options to help you, including renaming, undefining and changing visibility. The 'select' clause lets you resolve an ambiguity which arises in the case of dynamic binding.
A interesting case of inheritance is class MON12 (Figure 8),
which encapsulates the behaviour of the month component of a date. It implements
one of the key mechanisms of this cluster, allowing the month to determine the
highest value the day may assume.
class
MON12
-- Month of year, a component of a Gregorian date
inherit
DIGIT12
redefine
symbol, increment, decrement
end
creation
make,
connect_day
feature
connect_day (dd: DIGITV) is
-- Link to less significant digit
do
day := dd
changed
end
increment is
-- As parent, but note change
do
Precursor
changed
end -- increment
decrement is
-- As parent, but note change
do
Precursor
changed
end -- decrement
full_name: STRING is
-- Full name, in English
do
Result := full_names.item (value + 1)
end -- full_name
symbol: STRING is
-- Full name
do
Result := full_name
end -- symbol
feature {NONE} -- new features, private
day: DIGITV
-- Day of month, we control range
-- Less significant digit
changed is
-- Month has changed: update day range
do
if day /= Void then
inspect value + 1
when 1, 3, 5, 7, 8, 10, 12 then
day.set_range (1, 31)
when 2 then
day.set_range (1, 28) -- leap?
when 4, 6, 9, 11 then
day.set_range (1, 30)
end
end
end -- changed
full_names: ARRAY [STRING] is
-- Full month names
once
Result := <<"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December">>
end
end -- class MON12
Figure 8
A hidden feature changed keeps the day and month in step, and
must be called whenever the month is incremented or decremented. Features
increment and decrement do this, so we need to add one
statement to each of these features.
As far as it goes, the behaviour of increment and
decrement inherited from DIGIT is fine. Duplicating
the text here would introduce the kind of maintenance problem good
object-oriented programming avoids, as well as giving us more work to do.
We must certainly redefine the inherited increment and
decrement, but we want our new versions to call the inherited
versions too. The solution is to call Precursor, which invokes the
inherited version of the present routine.
A library class STD_FILES provides convenient access to the
standard input and output files. In Eiffel, all classes inherit from class
ANY. Even if no inheritance is specified, the class will implicitly
inherit from ANY, and thus class ANY represents the
top of the inheritance hierarchy for any user-defined classes. ANY
itself inherits from class GENERAL which provides several features
that are considered universally useful, regardless of the class you are writing.
These features are then automatically inherited into every class you write. One
of these is a feature of type STD_FILES called io. The
universally available io feature provides us with a convenient way
to do console input and output.
Figure 9 shows a class FILTER, which copies input to output one line at a
time. It uses features read_line, put_string and
last_string from class STD_FILES.
class
FILTER
creation
run
feature
run is
-- Echo until end of file
do
from
io.read_line
until
equal (io.last_string, "")
loop
io.put_string (io.last_string)
io.put_new_line
io.read_line
end -- loop
end -- run
end -- class FILTER
Figure 9
Consider the declaration of the feature io from class
GENERAL above. The short form of the feature will be as follows:
io: STD_FILES
In keeping with the principle of uniform access, we cannot tell from the short form whether this is an attribute or a function with no arguments. Which would be best?
Implementing io as an attribute would work but would be quite
inconvenient, as every class would have to create the STD_FILES
object before using it:
!!io.make
Implementing io as a function will remove the necessity to have
to create the object in descendants:
io: STD_FILES is
-- used for console I/O
do
!!Result.make
end -- io
This is more convenient as we can just use io whenever we want.
It does have one significant drawback though. Every time we use the
io feature, a new object of type STD_FILES gets
created. This will lead to an unnecessary profusion of objects. The Eiffel
solution in such a situation is to use a once function. The actual
definition of io in class GENERAL is as follows:
io: STD_FILES is
-- used for console I/O
once
!!Result.make
end -- io
The first time any routine in this system refers to 'io' it executes
normally, creating a new STD_FILES object and returning a reference
in Result. After that, any call of feature 'io' returns the same value without
executing the body again. In effect, 'io' behaves like a function the first time
and a read-only attribute thereafter.
The once routine provides a safe and elegant alternative to a variety of traditional mechanisms, notably constants of class types, initialisation routines, global and shared variables. Program design is simpler, and more versatile and flexible structures become possible.
Objects are created by creation instructions, but we have not tackled the question of how they die.
The obvious solution is to provide a means for the programmer to kill them. This is easy for the language implementor, but leads to serious difficulties for the application programmer.
Objects created in one part of the system may be destined for use elsewhere. The code responsible for creating an object may not be aware when it is no longer needed. When one part of the system has finished with an object it cannot be certain it is not still being used elsewhere.
Objects might contain mutual references to each other even after they have been discarded by the application which used them.
These considerations make memory management a burden too heavy for the programmer, and Eiffel provides no 'delete' or 'free' statement by which the programmer can cause objects to die. Instead, the Eiffel runtime system automatically tracks references to objects and reclaims space when it is safe to do so. The process is called 'garbage collection'.
You can help the garbage collector by explicitly discarding references which
you no longer need by assigning Void to them, but most references
(not objects) vanish at routine exit anyway.
There are situations in which even the garbage collector's carefully controlled interruption cannot be tolerated, and for such applications classes in the basic libraries contains features allowing you to control it.
An executing Eiffel system represents application data as a collection of collaborating objects, connected by a potentially complex web of references.
With current technology, these objects occupy volatile memory in the computer, and vanish when the system terminates. Persistence extends the lifetime of an object from one execution of a system to the next. It is achieved by saving the object to a file on secondary storage in one session and restoring it from the file in another.
In Eiffel, persistence is achieved using the library class
STORABLE. The two main features of interest from this class are
basic_store and retrieved. Their short forms are as
follows:
basic_store (file: FILE)
-- Produce on 'file' an external representation of the
-- entire object structure reachable from current object.
-- Retrievable within current system only
require
file_not_void: file /= Void
file_exists: file.exists
file_is_open_write: file.is_open_write
retrieved (file: FILE) : STORABLE
-- Retrieved object structure, from external
-- representation previously stored in 'file'.
-- To access resulting object under correct type,
-- use assignment attempt.
-- Will raise an axception (code Retrieve_exception)
-- if file content is not a STORABLE structure
require
file_not_void: file /= Void
file_exists: file.exists
file_is_open_read: file.is_open_read
ensure
result_exists : Result /= Void
To save an object, call feature basic_store giving a
FILE object as argument. The object is converted to a suitable
(private) external representation and stored in a new file with the specified
name. For example,
data_file: FILE
...
data_file.make_create_read_write ("prefer.dat")
w_preferences.basic_store (data_file)
It is not only the object referred to by 'w_preferences' which is saved, but
all the other objects on which it depends directly or indirectly. The entire
structure is retrieved at once by the corresponding retrieved
feature.
retrieved (data_file)
Detaching an object from the context in which it was created is a risky operation, since the language system can no longer monitor and enforce the typing rules.
The retrieved object is returned by a function with return type
STORABLE. The actual dynamic type may be any descendant of
STORABLE but this can only be determined at run-time. It is
regarded with suspicion, and in order to assume its real type again it must face
the challenge of an assignment attempt.
w_choices: USER_PREF
...
w_choices ?= retrieved (data_file)
An assignment attempt (the ?= operator) only succeeds if the source object has the same type as the target reference. More strictly, the dynamic type of the source expression must conform to the static type of the target entity.
Failure of this test is not treated as an error; the target simply becomes
void. In this case, if retrieved doesn't refer to a
USER_PREF object the assignment won't work and
w_choices becomes void.
More flexible and selective facilities are offered by classes in the basic libraries such as ENVIRONMENT, FILE and FILE_SYSTEM, but the details are platform-dependent.
The assignment attempt is used in any situation where an object whose general type is known needs to be treated as a more specific type.
Debuggers, browsers and mixed language programming tasks require access to the internal representation of objects.
Such access is provided either by a low-level C callable interface to the Eiffel runtime system, or by basic library classes. It is not defined as part of the language.
Obviously, such power is not for casual use, and ordinary applications will not need it.
There are a few kinds of error which cannot reasonably be dealt with in the normal flow of control. These include hardware errors and exhaustion of resources like disk space or memory. Eiffel's exception facility provides a properly disciplined defence against such unexpected events.
The idea of 'programming by contract' underlies the use of preconditions, postconditions and the class invariant. It assigns responsibility clearly between a routine and its caller.
The caller is responsible for satisfying the precondition, and the routine is then responsible for satisfying the postcondition and maintaining the invariant.
A routine may sometimes be prevented from completing by events outside its control. In this case the routine fails and Eiffel's exception handler takes over.
The failure of a called routine causes the caller to fail too, so the exception propagates up the call stack to the root object's creation procedure. The runtime system might dump a traceback to the screen or a file to identify the point of failure.
This escalation can be intercepted at any stage by an application exception handler. Such a handler must be intimately familiar with the context of the failure; in Eiffel it is associated with the routine rather than the class or system, in the form of a 'rescue' clause.
A routine can end only by fulfilling its contract or failing. A rescue clause is part of the routine, and has only two courses of action open to it. It may correct the cause of failure and try again, or it can clean up and concede defeat.
In order to devise sensible recovery strategies in critical routines, the
application might need information about the nature of the exception. Support
library class EXCEPTIONS provides such information, as well as
facilities to tune the exception mechanism.
Class STD_FILES illustrates another aspect of the Eiffel style,
the division of features into queries and commands.
Feature read_line does not return a value. It is a command,
intended to perform an action and modify the internal state of the object. The
procedure attempts to consume characters from the standard input until the end
of the current line has been seen. The attempt might succeed or fail; either
way, read_line has done the job it contracted to do.
Feature last_string returns a value. The feature might be a
function or an attribute - we can't tell, and it doesn't really matter to the
caller. If a call to read_line has succeeded in reading a line, the
value returned by last_string is that line. Otherwise,
last_string returns an empty string.
Feature last_string is a query. Calling it has no permanent
effect on the object. If you call last_string several times without
calling read_line you get the same string each time.
On the other hand, each call to read_line reads another line
(all being well). If you call read_line five times then five lines
will be read, regardless of any intervening calls to last_string.
Separating commands from queries leads to classes which are easier to use. They remember their own state, so the caller doesn't have to. The logic of the caller can become more relaxed and clearer.
This paper has taken a tour through some features of the Eiffel language, and tried to show why they were designed and how they are used. Treatment of some topics has necessarily been brief, but the aim has been to give you an impression of what it is like to develop software in Eiffel.
If your interest has been aroused, you should read 'Object Oriented Software Construction, Second Edition' by Bertrand Meyer, published by Prentice Hall in 1997 (ISBN 0-13-629155-0). The definitive language manual is 'Eiffel - The Language' by Bertrand Meyer, published by Prentice Hall in 1992 (ISBN 0-13-247925).
This paper was originally published as a four-part series in the newsletter of the UK and Ireland Eiffel Interest Group. It has been revised and reformatted several times, recently with help from Don Ryan of the Dublin Institute of Technology. Thanks to all who have contributed.