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.