Here you can learn about the Eiffel development method and the Eiffel programming language.
At the heart of the Eiffel Development Framework is the Eiffel method. Everything else -- the language, the tools, the libraries -- exists because of the method.
In order for us to communicate effectively about the activities of the method we must have some way to record the products of those activities. That is what the Eiffel programming language does for us ... with the added benefit that, ultimately, we implement whole running software systems in the language.
The method is laid out in wonderful detail in Object-oriented Software Construction, 2nd Edition. But, if you're just getting started, you will find some good introductory material on this page. The invitation to Eiffel is fairly short introduction to the method and language. and the tutorial gives a more detailed look.
The Invitation to Eiffel (I2E) is a short set of pages that should provide you with the essence of the Eiffel way, without taking too much of your time. Enjoy this profoundly different way of thinking about developing software! When you are done, move on to the more detailed Eiffel tutorial.
This Invitation assumes that you have some experience of software development, but that's all. Previous exposure to object technology is not required. If you've had it, it will help; but if it has all been to notations like UML or programming languages like C++ and Java, you should not let it guide your study of this Invitation. Although Eiffel shares a number of properties with these other approaches, it takes a fresh path to object technology, based on a small number of simple, far-reaching concepts.
Once you are familiar with the basic ideas you may want to try them with EiffelStudio, which provides a direct implementation of the Eiffel concepts, available in a completely portable way across Windows, Linux, many versions of Unix and OpenVMS.
The aim of Eiffel is to help specify, design, implement and modify quality software. This goal of quality in software is a combination of many factors; the language design concentrated on the three factors which, in the current state of the industry, are in direct need of improvements: reusability, extendibility and reliability. Also important were other factors such as efficiency, openness and portability.
Reusability is the ability to produce components that may serve in many different applications. Central to the Eiffel approach is the presence of predefined libraries such as EiffelBase, and the language's support for the production of new libraries.
Extendibility is the ability to produce easily modifiable software. "Soft" as software is supposed to be, it is notoriously hard to modify software systems, especially large ones.
Among quality factors, reusability and extendibility play a special role: satisfying them means having less software to write -- and hence more time to devote to other important goals such as efficiency, ease of use or integrity.
The third fundamental factor is reliability, the ability to produce software that is correct and robust -- that is to say, bug-free. Eiffel techniques such as static typing, assertions, disciplined exception handling and automatic garbage collection are essential here.
Three other factors are also part of Eiffel's principal goals:
To achieve reusability, extendibility and reliability, the principles of object-oriented design provide the best known technical answer.
An in-depth discussion of these principles fall beyond the scope of this introduction but here is a short definition:
Info: Object-oriented design is the construction of software systems as structured collections of abstract data type implementations, or "classes".
The following points are worth noting in this definition:
Eiffel makes these techniques available to developers in a simple and practical way.
As a language, Eiffel includes more than presented in this introduction, but not much more; it is a small language, not much bigger (by such a measure as the number of keywords) than Pascal. It was meant to be a member of the class of languages which programmers can master entirely -- as opposed to languages of which most programmers know only a subset. Yet it is appropriate for the development of industrial software systems, as has by now been shown by many full-scale projects, some in the thousands of classes and hundreds of thousands of lines, in companies around the world.
A class, it was said above, is an implementation of an abstract data type. This means that it describes a set of run-time objects, characterized by the features (operations) applicable to them, and by the formal properties of these features.
Such objects are called the direct instances of the class. Classes and objects should not be confused: "class" is a compile-time notion, whereas objects only exist at run time. This is similar to the difference that exists in classical programming between a program and one execution of that program, or between a type and a run-time value of that type.
Info: "Object-Oriented" is a misnomer; "Class-Oriented Analysis, Design and Programming" would be a more accurate description of the method.
To see what a class looks like, let us look at a simple example, ACCOUNT, which describes bank accounts. But before exploring the class itself it is useful to study how it maybe used by other classes, called it's clients.
A class X may become a client of ACCOUNT by declaring one or more entities of type ACCOUNT. Such a declaration is of the form:
acc: ACCOUNT
The term "entity" generalizes the more common notion of "variable". An entity declared of a reference type, such as acc, may at any time during execution become " attached to " an object; the type rules imply that this object must be a direct instance of ACCOUNT -- or, as seen below, of a "descendant" of that class.
An entity is said to be void if it is not attached to any object. By default, entities are void at initialization. To obtain objects at run-time, a routine r appearing in the client class X may use a creation instruction of the form
create acc
which creates a new direct instance of ACCOUNT, attaches acc to that instance, and initializes all its fields to default values. A variant of this notation, studied below, makes it possible to override the default initializations.
Once the client has attached acc to an object, it may call on this object the features defined in class ACCOUNT. Here is an extract with some feature calls using acc as their target:
acc.open ("Jill") acc.deposit (5000) if acc.may_withdraw (3000) then acc.withdraw (3000) print (acc.balance) end
These feature calls use dot notation, of the form target_name.feature_name, possibly followed by a list of arguments in parentheses. Features are of two kinds:
open, deposit, may_withdraw, withdraw, represent computations applicable to instances of the class.
Routines are further divided into procedures (commands, which do not return a value) and functions (queries, returning a value). Here may_withdraw is a function returning a boolean; the other three-routines called are procedures.
Info: A note on syntax: you may separate instructions by semicolons, and indeed you should when, as on the next-to-last line of the example, two or more instructions appear on a line. But the language's syntax has been designed so that the semicolon is almost always optional, regardless of the layout. Indeed the practice is to omit it between instructions or declarations on separate lines, as this results in lighter, clearer software texts.
In class ACCOUNT, is feature balance an attribute, or is it a function with no argument? The above extract of the client class X doesn't say, and this ambiguity is intentional. A client of ACCOUNT must not need to know how class ACCOUNT delivers an account's balance when requested: by looking up a field present in each account object, or by calling a function that computes the balance from other fields. Choosing between these techniques is the business of class ACCOUNT, not anybody else's. Because such implementation choices are often changed over the lifetime of a project, it is essential to protect clients against their effects. This is known as the Uniform Access Principle, stating that the choice between representing a property through memory (an attribute) or through an algorithm (function) shall not affect how clients use it.
So much for how client classes will typically use ACCOUNT. Below is a first sketch of how class ACCOUNT itself might look. Line segments beginning with -- are comments. The class includes two feature clauses, introducing its features. The first begins with just the keyword feature, without further qualification; this means that the features declared in this clause are available (or "exported") to all clients of the class. The second clause is introduced by feature {NONE} to indicate that the feature that follows, called add, is available to no client. What appears between the braces is a list of client classes to which the corresponding features are available; NONE is a special class of the Kernel Library, which has no instances, so that add is in effect a secret feature, available only locally to the other routines of class ACCOUNT. So in a client class such as X, the call acc.add ( -3000 ) would be invalid.
class ACCOUNT feature balance: INTEGER owner: PERSON minimum_balance: INTEGER = 1000 open (who: PERSON) -- Assign the account to owner who. do owner := who end deposit (sum: INTEGER) -- Deposit sum into the account. do add (sum) end withdraw (sum: INTEGER) -- Withdraw sum from the account. do add (-sum) end may_withdraw (sum: INTEGER): BOOLEAN -- Is there enough money to withdraw sum? do Result := (balance >= sum + minimum_balance) end feature {NONE} add (sum: INTEGER) -- Add sum to the balance do balance := balance + sum end end -- ACCOUNT
Let us examine the features in sequence. The do ... end distinguishes routines from attributes. So here the class has implemented balance as an attribute, although, as noted, a function would also have been acceptable. Feature owner is also an attribute.
The language definition guarantees automatic initialization, so that the initial balance of an account object will be zero after a creation instruction. Each type has a default initial value: zero for INTEGER and REAL, false for BOOLEAN, null character for CHARACTER, and a void reference for reference types. The class designer may also provide clients with different initialization options, as will be seen below in a revised version of this example.
The other public features, withdraw, deposit, open, and may_withdraw are straight-forward routines. The special entity Result, used in may_withdraw, denotes the function result; it is initialized on function entry to the default value of the function's result type. You may only use Result in functions.
The secret procedure add serves for the implementation of the public procedures deposit and withdraw; the designer of ACCOUNT judged it too general to be exported by itself. The clause "= 1000" introduces minimum_balance as a constant attribute, which will not occupy any space in instances of the class; in contrast, every instance has a field for every non-constant attribute such as balance.
In Eiffel's object-oriented programming style any operation is relative to a certain object. A client invoking the operation specifies this object by writing the corresponding entity on the left of the dot, as acc in acc.open ("Jill"). Within the class, however, the "current" instance to which operations apply usually remains implicit, so that unqualified feature names, such as owner in procedure open or add in deposit, mean "the owner attribute or add routine relative to the current instance".
If you need to denote the current object explicitly, you may use the special entity Current. For example the unqualified occurrences of add appearing in the class text above are equivalent to Current. add.
In some cases, infix or prefix notation will be more convenient than dot notation. For example, if a class VECTOR offers an addition routine, most people will feel more comfortable with calls of the form v + w than with the dot-notation call v.plus (w). To make this possible it suffices to give the routine a "+" alias. The operation is still a normal routine call which can be invoked with either the infix form or the dot-notation.
The above simple example has shown the basic structuring mechanism of the language: the class. A class describes objects accessible to clients through an official interface comprising some of the class features. Features are implemented as attributes or routines; the implementation of exported features may rely on other, secret ones.
Eiffel is strongly typed for readability and reliability. Every entity is declared of a certain type, which may be either a reference type or an expanded type.
Any type T is based on a class, which defines the operations that will be applicable to instances of T. The difference between the two categories of type affects the semantics of an entity x declared of type T: for a reference type, the most common case, possible values for x are references to objects; for an expanded type, the values are objects. In both cases, the type rules guarantee that the objects will be instances of T.
A non-expanded class such as ACCOUNT yields a reference type. As a result, an entity of type ACCOUNT, such as acc in the earlier client example (see the declaration of acc and the accompanying picture as given in I2E: Classes ), denotes possible run-time references to objects of type ACCOUNT.
In contrast, the value of an entity acc declared of type expanded ACCOUNT is an object such as the one shown on the figure below, with no reference. The only difference with the earlier figure is that the value of acc is now an ACCOUNT object, not a reference to such an object. No creation instruction is needed in this case. (The figure does not show the PERSON object to which the owner field of the ACCOUNT object -- itself a reference -- is attached.)
An important group of expanded types, based on library classes, includes the basic types CHARACTER, DOUBLE, REAL, INTEGER, and BOOLEAN. Clearly, the value of an entity declared of type INTEGER should be an integer, not a reference to an object containing an integer value. Operations on these types are defined by prefix and infix operators such as "+" and "<".
As a result of these conventions, the type system is uniform and consistent: all types, including the basic types, are defined from classes, either as reference types or as expanded types.
In the case of basic types, for obvious reasons of efficiency, the compilation mechanism implements the standard arithmetic and boolean operations directly through the corresponding machine operations, not through routine calls. But this is only a compiler optimization, which does not hamper the conceptual homogeneity of the type edifice.
If classes are to deserve their definition as abstract data type implementations, they must be known not just by the available operations, but also by the formal properties of these operations, which did not yet appear in the preceding example.
Contents
|
Eiffel encourages software developers to express formal properties of classes by writing assertions, which may in particular appear in the following roles:
ACCOUNT may wish to permit a withdrawal operation only if it keeps the account's balance at or above the minimum. Preconditions are introduced by the keyword require.
ensure, express conditions that the routine (the supplier) guarantees on return, if the precondition was satisfied on entry.
invariant, and represents a general consistency constraint imposed on all routines of the class.
With appropriate assertions, the class ACCOUNT becomes:
class ACCOUNT create make feature ... Attributes as before: balance , minimum_balance , owner , open ... deposit (sum: INTEGER) -- Deposit sum into the account. require sum >= 0 do add (sum) ensure balance = old balance + sum end withdraw (sum: INTEGER) -- Withdraw sum from the account. require sum >= 0 sum <= balance - minimum_balance do add (-sum) ensure balance = old balance - sum end may_withdraw ... -- As before feature {NONE} add ... make (initial: INTEGER) -- Initialize account with balance initial. require initial >= minimum_balance do balance := initial end invariant balance >= minimum_balance end -- ACCOUNT
The notation old expression is only valid in a routine postcondition. It denotes the value the expression had on routine entry.
In its last version above, the class now includes a creation procedure, make. With the first version, clients used creation instructions such as create acc1 to create accounts; but then the default initialization, setting balance to zero, violated the invariant. By having one or more creation procedures, listed in the create clause at the beginning of the class text, a class offers a way to override the default initializations. The effect of
create acc1.make (5_500)
is to allocate the object (as with the default creation) and to call procedure make on this object, with the argument given. This call is correct since it satisfies the precondition; it will ensure the invariant.
Info: The underscore _ in the integer constant 5_500 has no semantic effect. The general rule is that you can group digits by sets of three from the right to improve the readability of integer constants.
Note that the same keyword, create, serves both to introduce creation instructions and the creation clause listing creation procedures at the beginning of the class.
A procedure listed in the creation clause, such as make, otherwise enjoys the same properties as other routines, especially for calls. Here the procedure make is secret since it appears in a clause starting with
feature {NONE}
so it would be invalid for a client to include a call such as
acc.make (8_000)
To make such a call valid, it would suffice to move the declaration of make to the first feature clause of class ACCOUNT, which carries no export restriction. Such a call does not create any new object, but simply resets the balance of a previously created account.
Syntactically, assertions are boolean expressions, with a few extensions such as the old notation. Also, you may split an assertion into two or more clauses, as here with the precondition of withdraw; this is as if you had separated the clauses with an and, but makes the assertion clearer, especially if it includes many conditions.
Assertions play a central part in the Eiffel method for building reliable object-oriented software. They serve to make explicit the assumptions on which programmers rely when they write software elements that they believe are correct. Writing assertions amounts to spelling out the terms of the contract which governs the relationship between a routine and its callers. The precondition binds the callers; the postcondition binds the routine.
The underlying theory of Design by Contract, the centerpiece of the Eiffel method, views software construction as based on contracts between clients (callers) and suppliers (routines), relying on mutual obligations and benefits made explicit by the assertions.
Assertions are also an indispensable tool for the documentation of reusable software components: one cannot expect large-scale reuse without a precise documentation of what every component expects (precondition), what it guarantees in return (postcondition) and what general conditions it maintains (invariant).
Documentation tools in EiffelStudio use assertions to produce information for client programmers, describing classes in terms of observable behavior, not implementation. In particular the Contract Form of a class, also called its "short form", which serves as its interface documentation, is obtained from the full text by removing all non-exported features and all implementation information such as do clauses of routines, but keeping interface information and in particular assertions. Here is the Contract Form of the above class:
class interface ACCOUNT create make feature balance: INTEGER ... deposit (sum: INTEGER) -- Deposit sum into the account. require sum >= 0 ensure balance = old balance + sum withdraw (sum: INTEGER) -- Withdraw sum from the account. require sum >= 0 sum <= balance - minimum_balance ensure balance = old balance - sum may_withdraw ... end -- ACCOUNT
This is not actual Eiffel, only documentation of Eiffel classes, hence the use of slightly different syntax to avoid any confusion ( interface class rather than class). In accordance with the Uniform Access Principle (in Classes), the output for balance would be the same if this feature were a function rather than an attribute.
You will find in EiffelStudio automatic tools to produce the Contract Form of a class. You can also get the Flat Contract form, based on the same ideas but including inherited features along with those introduced in the class itself. EiffelStudio can produce these forms, and other documentation views of a class, in a variety of output formats including HTML, so that collaborative projects can automatically post the latest versions of their class interfaces on the Internet or an Intranet.
Under EiffelStudio you may also set up compilation options, for the whole system or specific classes only, to evaluate assertions at run time, to uncover potential errors ("bugs"). EiffelStudio provides several levels of assertion monitoring: preconditions only, postconditions etc. When monitoring is on, an assertion which evaluates to true has no further effect on the execution. An assertion that evaluates to false will trigger an exception, as described next; unless you have written an appropriate exception handler, the exception will cause an error message and termination with a precise message and a call trace.
This ability to check assertions provides a powerful testing and debugging mechanism, in particular because the classes of the EiffelBase Libraries, widely used in Eiffel software development, are protected by carefully written assertions.
Run-time monitoring, however, is only one application of assertions, whose role as design and documentation aids, as part of theory of Design by Contract, exerts a pervasive influence on the Eiffel style of software development.
Whenever there is a contract, the risk exists that someone will break it. This is where exceptions come in.
Exceptions -- contract violations -- may arise from several causes. One is an assertion violation, if you've selected run-time assertion monitoring. Another is a signal triggered by the hardware or operating system to indicate an abnormal condition such as arithmetic overflow, or an attempt to create a new object when there's not enough memory available.
Unless a routine has made specific provision to handle exceptions, it will fail if an exception arises during its execution. This in turn provides one more source of exceptions: a routine that fails triggers an exception in its caller.
A routine may, however, handle an exception through a rescue clause. This optional clause attempts to "patch things up" by bringing the current object to a stable state (one satisfying the class invariant). Then it can terminate in either of two ways:
rescue clause may execute a retry instruction, which causes the routine to restart its execution from the beginning, attempting again to fulfill its contract, usually through another strategy. This assumes that the instructions of the rescue clause, before the retry, have attempted to correct the cause of the exception.
rescue clause does not end with retry, then the routine fails: it returns to its caller, immediately triggering an exception. (The caller's rescue clause will be executed according to the same rules.)
The principle is that a routine must either succeed or fail: it either fulfills its contract, or not; in the latter case it must notify its caller by triggering an exception.
Usually, only a few routines of a system will explicitly include a rescue clause. A routine that doesn't have an explicit rescue is considered to have an implicit one, which calls a routine default_rescue that by default does nothing, so that an exception will cause the routine to fail immediately, propagating the exception to the caller.
An example using the exception mechanism is a routine attempt_transmission that tries to transmit a message over a phone line. The actual transmission is performed by an external, low-level routine transmit; once started, however, transmit may abruptly fail, triggering an exception, if the line is disconnected. Routine attempt_transmission tries the transmission at most 50 times; before returning to its caller, it sets a boolean attribute successful to True or False depending on the outcome. Here is the text of the routine:
Initialization rules ensure that failures, a local entity, is set to zero on entry.
This example illustrates the simplicity of the mechanism: the rescue clause never attempts to achieve the routine's original intent; this is the sole responsibility of the body (the do clause). The only role of the rescue clause is to clean up the objects involved, and then either to fail or to retry.
This disciplined exception mechanism is essential for software developers, who need protection against unexpected events, but cannot be expected to sacrifice safety and simplicity to pay for this protection.
The division of roles in object technology is clear: of the two principal constituents of a system, object types and operations, the first dominates. Classes, representing object types, determines the structure of the software; every routine, representing an operations, belongs to a class.
In some circumstances it is useful to define an object that denotes an operation. This is especially useful if you want to build an object structure that refers to operations, so that you can later traverse the structure and execute the operations encountered. A typical application is event-driven programming for Graphical User Interfaces (GUI), including Web programming. In GUI programming you will want to record properties of the form
"When the user clicks this OK button, the system must update the file"each involves a control (here the OK button), an event (mouse click) and an operation (update the file). This can be programmed by having an "event loop", triggered for each event, which performs massive decision-making (if "The latest event was `left mouse click on button 23'" then "Appropriate instructions" else if ... and so on with many branches); but this leads to bulky software architectures where introducing any new control or event requires updating a central part of the code. It's preferable to let any element of the system that encounters a new control-event-operation association
[control, event, operation]
store it as a triple of objects into an object structure, such as an array or a list. Triples in that structure may come from different parts of the system; there is no central know-it-all structure. The only central element is a simple mechanism which can explore the object structure to execute each operation associated with a certain control and a certain event. The mechanism is not just simple; it's also independent of your application, since it doesn't need to know about any particular control, event or operation (it will find them in the object structure). So it can be programmed once and for all, as part of a library such as EiffelVision 2 for platform-independent graphics.
To build an object structure, we need objects. A control, an event are indeed objects. But an operation is not: it's program code -- a routine of a certain class.
Agents address this issue. An agent is an object that represents a routine, which can then be kept in an object structure. The simplest form of agent is written agent r, where r is a routine. This denotes an object. If your_agent is such an agent object, the call
your_agent.call ([a, b])
where a and b are valid arguments for r, will have the same effect as a direct call to r with arguments a and b. Of course, if you know that you want to call r with those arguments, you don't need any agents; just use the direct call r (a, b). The benefit of using an agent is that you can store it into an object structure to be called later, for example when an event-driven mechanism finds the agent in the object structure, associated with a certain control and a certain event. For this reason agents are also called delayed calls.
Info: The notation [a, b] denotes a sequence of elements, or tuple. The reason call needs a tuple as argument, whereas the direct call r (a, b) doesn't, is that call is a general routine (from the EiffelBase class ROUTINE, representing agents) applicable to any agent, whereas the direct call refers explicitly to r and hence requires arguments a and b of specific types. The agent mechanism, however, is statically typed like the rest of the language; when you call call, the type checking mechanism ensures that the tuple you pass as argument contains elements a and b of the appropriate types.
A typical use of agents with EiffelVision 2 is
ok_button.select_actions.extend (agent your_routine)
which says: "add your_routine to the list of operations to be performed whenever a select event (left click) happens on ok_button". ok_button.select_actions is the list of agents associated with the button and the event; in list classes, procedure extend adds an item at the end of a list. Here, the object to be added is the agent.
This enables the EiffelVision 2 event-handling mechanism to find the appropriate agent when it processes an event, and call call on that agent to trigger the appropriate routine. EiffelVision 2 doesn't know that it's your_routine; in fact, it doesn't know anything about your application. It simply finds an agent in the list, and calls call on it. For your part, as the author of a graphical application, you don't need to know how EiffelVision 2 handles events; you simply associate the desired agents with the desired controls and events, and let EiffelVision 2 do the rest.
Agents extend to many areas beyond GUIs. In numerical computation, you may use an agent to pass to an "integrator" object a numerical function to be integrated over a certain interval. In yet another area, you can use agents (as in the iteration library of EiffelBase) to program iterators : mechanisms that repetitively apply an arbitrary operation -- represented by an agent -- to every element of a list, tree or other object structure. More generally, agent embody properties of the associated routines, opening the way to mechanism for reflection, also called "introspection": the ability, during software execution, to discover properties of the software itself.
Building software components (classes) as implementations of abstract data types yields systems with a solid architecture but does not in itself ensure reusability and extendibility. Two key techniques address the problem: genericity (unconstrained or constrained) and inheritance. Let us look first at the unconstrained form.
To make a class generic is to give it formal generic parameters representing as unknown types, as in these examples from EiffelBase, an open-source library covering basic data structures and algorithms:
ARRAY [G] LIST [G] LINKED_LIST [G]
These classes describe data structures -- arrays, lists without commitment to a specific representation, lists in linked representation -- containing objects of a certain type. The formal generic parameter G denotes this type.
A class such as these doesn't quite yet describe a type, but a type template, since G itself denotes an unknown type. To derive a directly usable list or array type, you must provide a type corresponding to G, called an actual generic parameter; this may be either an expanded type, including basic types such as INTEGER, or a reference type. Here are some possible generic derivations:
As the last example indicates, an actual generic parameter may itself be generically derived.
It would not be possible, without genericity, to have static type checking in a realistic object-oriented language.
A variant of this mechanism, constrained genericity, will enable a class to place specific requirements on possible actual generic parameters. Constrained genericity will be described after inheritance.
Inheritance, the other fundamental generalization mechanism, makes it possible to define a new class by combination and specialization of existing classes rather than from scratch.
The following simple example, from the Data Structure Library in EiffelBase, is typical. LIST, as noted, describes lists in any representation. One such representation if the lists have a fixed number of elements uses an array. We may define the corresponding class by combination of LIST and ARRAY, as follows:
class ARRAYED_LIST [G] inherit LIST [G] ARRAY [G] export ... See below ... end feature ... Specific features of fixed-size lists ... end -- ARRAYED_LIST
The inherit ... clause lists all the "parents" of the new class, which is said to be their "heir". (The "ancestors" of a class include the class itself, its parents, grandparents etc.; the reverse term is "descendant".) Declaring ARRAYED_LIST as shown ensures that all the features and properties of lists and arrays are applicable to arrayed lists as well. Since the class has more than one parent, this is a case of multiple inheritance.
Standard graphical conventions -- drawn from the Business Object Notation or BON, a graphical object-oriented notation based on concepts close to those of Eiffel, and directly supported by EiffelStudio -- illustrate such inheritance structures:
An heir class such as ARRAYED_LIST needs the ability to define its own export policy. By default, inherited features keep their export status (publicly available, secret, available to selected classes only); but this may be changed in the heir. Here, for example, ARRAYED_LIST will export only the exported features of LIST, making those of ARRAY unavailable directly to ARRAYED_LIST 's clients. The syntax to achieve this is straightforward:
class ARRAYED_LIST [G] inherit LIST [G] ARRAY [G] export {NONE} all end ... The rest as above ...
Another example of multiple inheritance comes from a windowing system based on a class WINDOW, close to actual classes in EiffelVision 2. Windows have graphical features: a height, a width, a position, routines to scale windows, move them, and other graphical operations. The system permits windows to be nested, so that a window also has hierarchical features: access to sub windows and the parent window, adding a sub window, deleting a sub window, attaching to another parent and so on. Rather than writing complex class that would contain specific implementations for all of these features, it is preferable to inherit all hierarchical features from TREE (a class in EiffelBase describing trees), and all graphical features from a class RECTANGLE.
Inheritance complements the "client" relation by providing another form of reuse that yields remarkable economies of effort -- for analysis, design, implementation, evolution -- and has a profound effect on the entire software development process.
The very power of inheritance demands adequate means to keep it under control. Multiple inheritance, in particular, raises the question of name conflicts between features inherited from different parents; this case will inevitably arise in practice, especially for classes contributed by independent developers. You may remove such a name conflict through renaming, as in
class C inherit A rename x as x1, y as y1 end B rename x as x2, y as y2 end feature ...
Here, if both A and B have features named x and y, class C would be invalid without the renaming.
Renaming also serves to provide more appropriate feature names in descendants. For example, class WINDOW may inherit a routine insert_subtree from TREE. For clients of WINDOW, however, such a routine name is no longer appropriate. An application that uses this class needs coherent window terminology, and should have to concern itself with the inheritance structure that led to the class. So you may wish to rename insert_subtree as add_subwindow in the inheritance clause of WINDOW.
As a further protection against misusing multiple inheritance, the invariants of all parent classes automatically apply to a newly defined class. So classes may not be combined if their invariants are incompatible.
Inheritance is not just a module combination and enrichment mechanism. It also enables the definition of flexible entities that may become attached to objects of various forms at run time, a property known as polymorphism.
This remarkable facility must be reconciled with static typing. The language convention is simple: an assignment of the form a : = b is permitted not only if a and b are of the same type, but more generally if a and b are of reference types A and B, based on classes A and B such that B is a descendant of A.
This corresponds to the intuitive idea that a value of a more specialized type may be assigned to an entity of a less specialized type -- but not the reverse. (As an analogy, consider that if you request vegetables, getting green vegetables is fine, but if you ask for green vegetables, receiving a dish labeled just "vegetables" is not acceptable, as it could include, say, carrots.)
What makes this possibility particularly powerful is the complementary facility: feature redefinition. A class may redefine some or all of the features which it inherits from its parents. For an attribute or function, the redefinition may affect the type, replacing the original by a descendant; for a routine it may also affect the implementation, replacing the original routine body by a new one.
Assume for example a class POLYGON, describing polygons, whose features include an array of points representing the vertices and a function perimeter which computes a polygon's perimeter by summing the successive distances between adjacent vertices. An heir of POLYGON may begin as:
Here it is appropriate to redefine perimeter for rectangles as there is a simpler and more efficient algorithm. Note the explicit redefine sub clause (which would come after the rename if present).
Other descendants of POLYGON may also have their own redefinitions of perimeter. The version to use in any call is determined by the run-time form of the target. Consider the following class fragment:
p: POLYGON r: RECTANGLE ... create p create r ... if c then p := r end print (p.perimeter)
The polymorphic assignment p := r is valid because of the above rule. If condition c is false, p will be attached to an object of type POLYGON for the computation of p. perimeter, which will thus use the polygon algorithm. In the opposite case, however, p will be attached to a rectangle; then the computation will use the version redefined for RECTANGLE. This is known as dynamic binding.
Dynamic binding provides a high degree of flexibility. The advantage for clients is the ability to request an operation (such as perimeter computation) without explicitly selecting one of its variants; the choice only occurs at run-time. This is essential in large systems, where many variants may be available; dynamic binding protects each component against changes in other components.
This technique is particularly attractive when compared to its closest equivalent in traditional approaches, where you would need records with variant components, or union types (C), together with case (switch) instructions to discriminate between variants. This means that every client must know about every possible case, and that any extension may invalidate a large body of existing software.
The combination of inheritance, feature redefinition, polymorphism and dynamic binding supports a development mode in which every module is open and incremental. When you want to reuse an existing class but need to adapt it to a new context, you can define a new descendant of that class (with new features, redefined ones, or both) without any change to the original. This facility is of great importance in software development, an activity that -- by design or circumstance -- is invariably incremental.
The power of these techniques demands adequate controls. First, feature redefinition, as seen above, is explicit. Second, because the language is typed, a compiler can check statically whether a feature application a.f is correct. In contrast, dynamically typed object-oriented languages defer checks until run-time and hope for the best: if an object "sends a message" to another (that is to say, calls one of its routines) one just expects that the corresponding class, or one of its ancestors, will happen to include an appropriate routine; if not, a run-time error will occur. Such errors will not happen during the execution of a type-checked Eiffel system.
In other words, the language reconciles dynamic binding with static typing. Dynamic binding guarantees that whenever more than one version of a routine is applicable the right version (the one most directly adapted to the target object) will be selected. Static typing means that the compiler makes sure there is at least one such version.
This policy also yields an important performance benefit: in contrast with the costly run-time searches that may be needed with dynamic typing (since a requested routine may not be defined in the class of the target object but inherited from a possibly remote ancestor), the EiffelStudio implementation always finds the appropriate routine in constant-bounded time.
Assertions provide a further mechanism for controlling the power of redefinition. In the absence of specific precautions, redefinition may be dangerous: how can a client be sure that evaluation of p.perimeter will not in some cases return, say, the area? Preconditions and postconditions provide the answer by limiting the amount of freedom granted to eventual redefiners. The rule is that any redefined version must satisfy a weaker or equal precondition and ensure a stronger or equal postcondition than in the original. This means that it must stay within the semantic boundaries set by the original assertions.
The rules on redefinition and assertions are part of the Design by Contract theory, where redefinition and dynamic binding introduce subcontracting. POLYGON, for example, subcontracts the implementation of perimeter to RECTANGLE when applied to any entity that is attached at run-time to a rectangle object. An honest subcontractor is bound to honor the contract accepted by the prime contractor. This means that it may not impose stronger requirements on the clients, but may accept more general requests: weaker precondition; and that it must achieve at least as much as promised by the prime contractor, but may achieve more: stronger postcondition.
Genericity and inheritance, the two fundamental mechanisms for generalizing classes, may be combined in two fruitful ways.
The first technique yields polymorphic data structures. Assume that in the generic class LIST [G] the insertion procedure put has a formal argument of type G, representing the element to be inserted. Then with a declaration such as
pl: LIST [POLYGON]
the type rules imply that in a call pl.put (p) the permitted types for the argument p include not just POLYGON, but also RECTANGLE (an heir of POLYGON) or any other type conforming to POLYGON through inheritance.
The basic conformance requirement used here is the inheritance-based type compatibility rule: V conforms to T if V is a descendant of T.
Structures such as pl may contain objects of different types, hence the name "polymorphic data structure". Such polymorphism is, again, made safe by the type rules: by choosing an actual generic parameter ( POLYGON in the example) based higher or lower in the inheritance graph, you extend or restrict the permissible types of objects in pl. A fully general list would be declared as
where ANY, a Kernel Library class, is automatically an ancestor of any class that you may write.
The other mechanism for combining genericity and inheritance is constrained genericity. By indicating a class name after a formal generic parameter, as in
VECTOR [T -> NUMERIC]
you express that only descendants of that class (here NUMERIC) may be used as the corresponding actual generic parameters. This makes it possible to use the corresponding operations. Here, for example, class VECTOR may define a routine infix "+" for adding vectors, based on the corresponding routine from NUMERIC for adding vector elements. Then by making VECTOR itself inherit from NUMERIC, you ensure that it satisfies its own generic constraint and enable the definition of types such as VECTOR [VECTOR [T]] .
As you have perhaps guessed, unconstrained genericity, as in LIST [G] , may be viewed as an abbreviation for genericity constrained by ANY, as in
Something else you may have guessed: if ANY, introduced in this session, is the top of the inheritance structure -- providing all classes with universal features such as equal to compare arbitrary objects and twin to duplicate objects -- then NONE, seen earlier in the notation feature {NONE}, is its bottom. NONE indeed conceptually inherits from all other classes. NONE is, among other things, the perceived type of the Void keyword which represents a void reference.
The inheritance mechanism includes one more major notion: deferred features and classes.
Declaring a feature f as deferred in a class C expresses that there is no default implementation of f in C; such implementations will appear in eventual descendants of C. A class that has one or more deferred routines is itself said to be deferred. A non-deferred routine or class -- like all those seen until now -- is said to be effective.
For example, a system used by a Department of Motor Vehicles to register vehicles might include a class of the form
deferred class VEHICLE feature dues_paid (year: INTEGER): BOOLEAN do ... end valid_plate (year: INTEGER): BOOLEAN do ... end register (year: INTEGER) -- Register vehicle for year. require dues_paid (year) deferred ensure valid_plate (year) end ... Other features, deferred or effective ... end -- VEHICLE
This example assumes that no single registration algorithm applies to all kinds of vehicle; passenger cars, motorcycles, trucks etc. are all registered differently. But the same precondition and postcondition apply in all cases. The solution is to treat register as a deferred routine, making VEHICLE a deferred class. Descendants of class VEHICLE, such as CAR or TRUCK, effect this routine, that is to say, give effective versions. An effecting is similar to a redefinition; only here there is no effective definition in the original class, just a specification in the form of a deferred routine. The term redeclaration covers both redefinition and effecting.
Whereas an effective class described an implementation of an abstract data types, a deferred class describes a set of possible implementations. You may not instantiate a deferred class: create v is invalid if v is declared of type VEHICLE. But you may assign to v a reference to an instance of an effective descendant of VEHICLE. For example, assuming CAR and TRUCK provide effective definitions for all deferred routines of VEHICLE, the following will be valid:
v: VEHICLE c: CAR t: TRUCK ... create c create t ... if "Some test" then v := c else v := t end v.register (2008)
This example fully exploits polymorphism: depending on the outcome of "Some test", v will be treated as a car or a truck, and the appropriate registration algorithm will be applied. Also, "Some test" may depend on some event whose outcome is impossible to predict until run-time, for example the user clicking with the mouse to select one among several vehicle icons displayed on the screen.
Deferred classes are particularly useful at the design stage. The first version of a module may be a deferred class, which will later be refined into one or more effective classes. Eiffel's Design by Contract mechanisms are essential here: you may a precondition and a postcondition with a routine even though it is a deferred routine (as with register above), and an invariant with a class even though it is a deferred class. This enables you, as a designer, to attach precise semantics to a module at the design stage long before you will make any implementation choices.
Beyond design and implementation, these techniques extend to the earliest stage of development, analysis. Deferred classes written at that stage describe not software objects, but objects from the external world being modeled -- documents, airplanes, investments. Here again the presence of contracts to express constraints, and the language's other structuring facilities, provide an attractive combination.
Eiffel appears here in its full role of a lifecycle approach, covering areas traditionally considered separate: program implementation, the traditional province of development environments; system modeling and architecture, the traditional province of CASE tools based on UML or similar notations disconnected from the rest of the lifecycle. Eiffel instead emphasizes the fundamental unity of the software process and the usefulness of a single set of notations, concepts and tools applicable throughout. Such a seamless approach is indispensable to support the inevitable reversals that occur during the process of building software, such as detecting at implementation time a problem that leads to a change in the system's functionality, set at analysis time. The use of separate tools and notations, such as UML on one side and a programming language on the other, makes such round-trips difficult at best and often leads to monolithic, hard-to-change software. Eiffel lets you focus on the issues, without interposing artificial barriers between different software development activities. You'll use the fundamental problem-solving techniques -- data abstraction through classes, precise specification through contracts, modularity through information hiding, rational organization through inheritance, decentralized architecture through dynamic binding, parameterization of the solution through genericity, reusability through all these techniques -- all along; only the level of abstraction changes.
We have now studied the constituents of Eiffel software. It remains to see how you can combine these elements into executable systems (the Eiffel concept closest to the traditional notion of program) and libraries.
How do you get an executable system? All you need is to
This defines what it means to execute the system: create one direct instance of the root class (the execution's root object); and call the root procedure on it. That's all.
In any practical case, the root procedure will create other objects, call other routines on them, leading to further creations and calls.
For the system to be valid, it must include all the classes which the root needs directly or indirectly; a class "needs" another if it is one of its heirs or clients.
For a library we don't need to specify a root. If we want to make sure that every class in a library compiles fine we can specify that we want all classes to be the root.
The Eiffel method suggests grouping related classes (typically 5 to 40 classes) into collections called clusters. A universe is then a set of clusters. For example the EiffelBase library is divided into clusters corresponding each to a major category of data structure: lists, tables, iteration, and so on. You can nest clusters, using for example EiffelBase, with its own subclusters as listed, as a cluster of your system.
How will you specify a universe? Any Eiffel implementation can use its own conventions. EiffelStudio applies a simple policy:
name.e . For clarity, name should be the lower-case version of the class name, although this is a style rule, not a requirement.
Note: It is desirable for clarity, as a style rule, to separate clusters that directly contain classes ("terminal clusters") from those that have subclusters. Cluster directories will then contain class files or cluster subdirectories, but not both.
Such a system specification is written in an ecf (eiffel configuration file) which is an xml based file format. It can be created by using the project settings in EiffelStudio.
Title: Invitation to Eiffel, Eiffel Software Technical Report TR-EI-67/IV.
Contents
|
First published 1987. Some revisions (in particular Web versions) have used the title "Eiffel in a Nutshell"
This version: July 2001. Introduces coverage of agents; several other improvements. Corresponds to release 5.0 of the EiffelStudio environment.
Bertrand Meyer
See acknowledgments in book Eiffel: The Language.
Rich Ayling.
Copyright Interactive Software Engineering Inc. (Eiffel Software), 2001. May not be reproduced in any form (including electronic storage) without the written permission of Eiffel Software . "Eiffel Power" and the Eiffel Power logo are trademarks of Eiffel Software . All uses of the product documented here are subject to the terms and conditions of the Eiffel Software Eiffel user license. Any other use or duplication is a violation of the applicable laws on copyright, trade secrets and intellectual property.
Degree-granting educational institutions using Eiffel Software Eiffel for teaching purposes as part of the Eiffel University Partnership Program may be permitted under certain conditions to copy specific parts of this book. Contact Eiffel Software for details.
Info: About Eiffel Software (Interactive Software Engineering) helps you produce software better, faster and cheaper. Eiffel Software provides a wide range of products and services based on object technology, including Eiffel Software Eiffel, a complete development environment for the full system life cycle. Eiffel Software's training courses, available worldwide, cover key management and technical topics. Eiffel Software's consultants are available to address your project needs at all levels. Eiffel Software's TOOLS (Technology of Object-Oriented Languages and Systems) conferences, http://www.tools-conferences.com , are the meeting point for anyone interested in the software technologies of the future. Eiffel Software originated one of the earliest . NET products and offers a full range of . NET services and training at http://www.dotnetexperts.com .
Interactive Software Engineering Inc.
Eiffel Software Building,
356 Storke Road,
Goleta,
CA, 93117
USA.
Telephone 805-685-1006,
Fax 805-685-6869
Eiffel Software maintains a rich source of information at http://eiffel.com , with more than 1200 Web pages including online documentation, downloadable files, product descriptions, links to Eiffel Software partners, University Partnership program, mailing list archives, announcements, press coverage, Frequently Asked Questions, Support pages, and much more. Visit http://www.eiffel.com/general/contact_details.html to request information about products and services. To subscribe to the Eiffel Software Eiffel user list, go to http://groups.eiffel.com/join .
Eiffel Software offers a variety of support options tailored to the diverse needs of its customers. See http://support.eiffel.com for details.
This Eiffel Tutorial (ET) should provide you with a broad understanding of what Eiffel is all about and why it is different from other technologies. Still more detail is available in Object-Oriented Software Construction, 2nd Edition.
Eiffel is a method and language for the efficient description and development of quality systems.
As a language, Eiffel is more than a programming language. It covers not just programming in the restricted sense of implementation but the whole spectrum of software development:
Although the language is the most visible part, Eiffel is best viewed as a method, which guides system analysts and developers through the process of software construction. The Eiffel method is focused on both productivity (the ability to produce systems on time and within budget) and quality, with particular emphasis on the following quality factors:
Here is an overview of the facilities supported by Eiffel:
It is also useful, as in any design, to list some of what is not present in Eiffel. The approach is indeed based on a small number of coherent concepts so as to remain easy to master. Eiffel typically takes a few hours to a few days to learn, and users seldom need to return to the reference manual once they have understood the basic concepts. Part of this simplicity results from the explicit decision to exclude a number of possible facilities:
Eiffel, as noted, supports the entire lifecycle. The underlying view of the system development lifecycle is radically different not only from the traditional "Waterfall" model (implying a sequence of discrete steps, such as analysis, global design, detailed design, implementation, separated by major changes of method and notation) but also from its more recent variants such as the spiral model or "rapid prototyping", which remain predicated on a synchronous, full-product process, and retain the gaps between successive steps.
Clearly, not everyone using Eiffel will follow to the letter the principles outlined below; in fact, some highly competent and successful Eiffel developers may disagree with some of them and use a different process model. In the author's mind, however, these principles fit best with the language and the rest of the method, even if practical developments may fall short of applying their ideal form.
Contents
|
Unlike earlier approaches, the Eiffel model assumes that the system is divided into a number of subsystems or clusters. It keeps from the Waterfall a sequential approach to the development of each cluster (without the gaps), but promotes concurrent engineering for the overall process, as suggested by the following picture:
The Eiffel techniques developed below, in particular information hiding and Design by Contract, make the concurrent engineering process possible by letting the clusters rely on each other through clearly defined interfaces, strictly limiting the amount of knowledge that one must acquire to use the cluster, and permitting separate testing. When the inevitable surprises of a project happen, the project leader can take advantage of the model's flexibility, advancing or delaying various clusters and steps through dynamic reallocation of resources.
Each of the individual cluster life cycles is based on a continuous progression of activities, from the more abstract to the more implementation-oriented:
You may view this picture as describing a process of accretion (as with a stalactite), where each steps enriches the results of the previous one. Unlike traditional views, which emphasize the multiplicity of software products -- analysis document, global and detailed design documents, program, maintenance reports... --, the principle is here to treat the software as a single product which will be repeatedly refined, extended and improved. The Eiffel programming language supports this view by providing high-level notations that can be used throughout the lifecycle, from the most general and software-independent activities of system modeling to the most exacting details of implementation tuned for optimal run-time performance.
These properties make Eiffel span the scope of both "object-oriented methods", with their associated notations such as UML and supporting CASE tools (whereas most such solutions do not yield an executable result), and "programming languages" (whereas most such languages are not suitable for design and analysis).
Additionally, within the EiffelStudio development environment, the concept of single product is extended to documents external to the software itself, by the Eiffel Information System (EIS) which allows the linking elements of the software text to portions of external documents and vice versa.
The preceding ideas define the seamless approach embodied by Eiffel. With seamlessness goes reversibility: the ability to go back, even late in the process, to earlier stages. Because the developers work on a single product, they can take advantages of bouts of late wisdom -- such as a great idea for adding a new function, discovered only at implementation time -- and integrate them in the product. Traditional approaches tend to discourage reversibility because it is difficult to guarantee that the analysis and design will be updated with the late changes. With the single-product principle, this is much easier to achieve.
Seamlessness and reversibility enhance extendibility by providing a direct mapping from the structure of the solution to the structure of the problem description, making it easier to take care of customers' change requests quickly and efficiently. They promote reliability, by avoiding possible misunderstandings between customers' and developers' views. They are a boost to maintainability. More generally, they yield a smooth, consistent software process that helps both quality and productivity.
The last step of the cluster, Generalization, is unheard of in traditional models. Its task is to prepare the results of a cluster for reuse across projects by looking for elements of general applicability, and transform them for inclusion in libraries.
Recent object-oriented literature has used the term "refactoring" to describe a process of continuous improvement of released software. Generalization includes refactoring, but also pursues a more ambitious goal: helping turn program elements (software modules useful only as part of a certain program) into software components -- reusable parts with a value of their own, ready to be used by diverse programs that can benefit from their capabilities.
Of course not all companies using the method will be ready to include a Generalization phase in their. But those which do will see the reusability of their software greatly improved.
Complementing the preceding principles is the idea that, in the cluster lifecycle, the development team (under the responsibility of the project leader) should at all times maintain a current working demo which, although covering only a part of the final system, works well, and can be demonstrated or -- starting at a suitable time -- shipped as an early release. It is not a "prototype" in the sense of a meant to be thrown away, but an initial iteration towards the final product; the successive iterations will progress continuously towards until they become that final product.
The preceding goals benefit from the ability to check frequently that the current iteration is correct and robust. Eiffel supports efficient compilation mechanisms through such mechanisms as the Melting Ice Technology in EiffelStudio. The Melting Ice achieves immediate recompilation after a change, guaranteeing a recompilation time that's a function of the size of the changes, not of the system's overall size. Even for a system of several thousand classes and several hundred thousand lines, the time to get restarted after a change to a few classes is, on a typical modern computer, a few seconds.
Such a "melt" (recompilation) will immediately catch (along with any syntax errors) the type errors -- often the symptoms of conceptual errors that, if left undetected, could cause grave damage later in the process or even during operation. Once the type errors have been corrected, the developers should start testing the new functionalities, relying on the power of assertions -- explained in "Design By Contract Assertions, Exceptions" -- to kill the bugs while they are still larvae. Such extensive unit and system testing, constantly interleaved with development, plays an important part in making sure that the "current demo" is trustworthy and will eventually yield a correct and robust product.
Throughout the process, the method suggests maintaining a constant quality level: apply all the style rules, put in all the assertions, handle erroneous cases (rather than the all too common practice of thinking that one will "make the product robust" later on), enforce the proper architecture. This applies to all the quality factors except possibly reusability (since one may not know ahead of time how best to generalize a component, and trying to make everything fully general may conflict with solving the specific problem at hand quickly). All that varies is functionality: as the project progresses and clusters come into place, more and more of the final product's intended coverage becomes available. The project's most common question, "Can we ship something yet?", translates into "Do we cover enough?", not "Is it good enough?" (as in "Will it not crash?").
Of course not everyone using Eiffel can, any more than in another approach, guarantee that the ideal just presented will always hold. But it is theoretical scheme to which the method tends. It explains Eiffel's emphasis on getting everything right: the grandiose and the mundane, the structure and the details. Regarding the details, the Eiffel books cited in the bibliography include many rules, some petty at first sight, about such low-level aspects as the choice of names for classes and features (including their grammatical categories), the indentation of software texts, the style for comments (including the presence or absence of a final period), the use of spaces. Applying these rules does not, of course, guarantee quality; but they are part of a quality-oriented process, along with the more ambitious principles of design. In addition they are particularly important for the construction of quality libraries, one of the central goals of Eiffel.
Whenever they are compatible with the space constraints, the present chapter and the rest of this book apply these rules to their Eiffel examples.
When discovering any approach to software construction, however ambitious its goals, it is reassuring to see first a small example of the big picture -- a complete program to print the famous "Hello World" string. Here is how to perform this fascinating task in the Eiffel notation.
You write a class HELLO with a single procedure, say make, also serving as creation procedure. If you like short texts, here is a minimal version:
class HELLO create make feature make do print ("Hello World%N") end end
In practice, however, the Eiffel style rules suggest a better documented version:
note description: "Root for trivial system printing a message" author: "Elizabeth W. Brown" class HELLO create make feature make -- Print a simple message. do io.put_string ("Hello World") io.put_new_line end end -- class HELLO
The two versions perform identically; the following comments will cover the more complete second one.
Note the absence of semicolons and other syntactic clatter or clutter. You may in fact use semicolons to separate instructions and declarations. But the language's syntax is designed to make the semicolon optional (regardless of text layout) and it's best for readability to omit it, except in the special case of successive elements on a single line.
The note clause does not affect execution semantics; you may use it to associate documentation with the class, so that browsers and other indexing and retrieval tools can help users in search of reusable components satisfying certain properties. Here we see two notes, labeled description and author.
The name of the class is HELLO. Any class may contain "features"; HELLO has just one, called make. The create clause indicates that make is a "creation procedure", that is to say an operation to be executed at class instantiation time. The class could have any number of creation procedures.
The definition of make appears in a feature clause. There may be any number of such clauses (to separate features into logical categories), and each may contain any number of feature declarations. Here we have only one.
The line starting with -- (two hyphen signs) is a comment; more precisely it is a "header comment", which style rules invite software developers to write for every such feature, just after the point at which the feature is named. As will be seen in "The contract form of a class", the tools of EiffelStudio know about this convention and use it to include the header comment in the automatically generated class documentation.
The body of the feature is introduced by the do keyword and terminated by end. It consists of two output instructions. They both use io, a generally available reference to an object that provides access to standard input and output mechanisms; the notation io.f, for some feature f of the corresponding library class (STD_FILES, in this case), means "apply f to io". Here we use two such features:
put_string outputs a string, passed as argument, here "Hello World".
put_new_line terminates the line.
Rather than using a call to put_new_line, the first version of the class simply includes a new-line character, denoted as %N (the percent sign is used to introduce codes for special characters), at the end of the string. Either technique is acceptable.
You may have noticed another difference between the two versions. The first version uses a call to print where the second uses io.put_string . Here too, the effect is identical and either technique is acceptable. In the next section, you will begin to see how things like io and print become available for use in a class like HELLO.
To build the system and execute it:
HELLO as the "root class" and make as the "root procedure".
hello.e in the current directory.
Execution starts and outputs Hello World on the appropriate medium: under Windows, a Console; under Unix or OpenVMS, the windows from which you started EiffelStudio.
We now look at the overall organization of Eiffel software.
References to Eiffel Software's libraries appearing in subsequent examples include: EiffelBase, the fundamental open-source library covering data structures and algorithms; the kernel library, a subset of EiffelBase covering the most basic notions such as arrays and strings; and EiffelVision 2, an advanced graphics and GUI library providing full compatibility across platforms (Unix, Windows, OpenVMS) with native look-and-feel on each.
Contents
|
An Eiffel system is a collection of classes, one of which is designated as the root class. One of the features of the root class, which must be one of its creation procedures, is designated as the root procedure.
To execute such a system is to create an instance of the root class (an object created according to the class description) and to execute the root procedure. In anything more significant than "Hello World" systems, this will create new objects and apply features to them, in turn triggering further creations and feature calls.
For the system to make sense, it must contains all the classes on which the root depends directly or indirectly. A class B depends on a class A if it is either a client of A, that is to say uses objects of type A, or an heir of A, that is to say extends or specializes A. (These two relations, client and inheritance, are covered below.)
The notion of class is central to the Eiffel approach. A class is the description of a type of run-time data structures (objects), characterized by common operations features) and properties. Examples of classes include:
ACCOUNT may have features such as deposit, adding a certain amount to an account, all_deposits, yielding the list of deposits since the account's opening, and balance, yielding the current balance, with properties stating that deposit must add an element to the all_deposits list and update balance by adding the sum deposited, and that the current value of balance must be consistent with the lists of deposits and withdrawals.
COMMAND in an interactive system of any kind may have features such as execute and undo , as well as a feature undoable which indicates whether a command can be undone, with the property that undo is only applicable if undoable yields the value true.
LINKED_LIST may have features such as put, which adds an element to a list, and count, yielding the number of elements in the list, with properties stating that put increases count by one and that count is always non-negative.
We may characterize the first of these examples as an analysis class, directly modeling objects from the application domain; the second one as a design class, describing a high-level solution; and the third as an implementation class, reused whenever possible from a library such as EiffelBase. In Eiffel, however, there is no strict distinction between these categories; it is part of the approaches seamlessness that the same notion of class, and the associated concepts, may be used at all levels of the software development process.
Two relations may exist between classes:
C as a client of a class A to enable the features of C to rely on objects of type A.
B as an heir of a class A to provide B with all the features and properties of A, letting B add its own features and properties and modify some of the inherited features if appropriate.
If C is a client of A, A is a supplier of C. If B is an heir of A, A is a parent of B. A descendant of A is either A itself or, recursively, a descendant of an heir of A; in more informal terms a descendant is a direct or indirect heir, or the class itself. To exclude A itself we talk of proper descendant. In the reverse direction the terms are ancestor and proper ancestor.
The client relation can be cyclic; an example involving a cycle would be classes PERSON and HOUSE, modeling the corresponding informal everyday "object" types and expressing the properties that every person has a home and every home has an architect. The inheritance (heir) relation may not include any cycle.
In modeling terms, client roughly represents the relation "has" and heir roughly represents "is". For example we may use Eiffel classes to model a certain system and express that every child has a birth date (client relation) and is a person (inheritance).
Distinctive of Eiffel is the rule that classes can only be connected through these two relations. This excludes the behind-the-scenes dependencies often found in other approaches, such as the use of global variables, which jeopardize the modularity of a system. Only through a strict policy of limited and explicit inter-class relations can we achieve the goals of reusability and extendibility.
An Eiffel class that you write does not come into a vacuum but fits in a preordained structure, shown in the figure and involving two library classes: ANY and NONE.
Any class that does not explicitly inherit from another is considered to inherit from ANY, so that every class is a descendant, direct or indirect, of ANY. ANY introduces a number of general-purpose features useful everywhere, such as copying, cloning and equality testing operations (see The Dynamic Structure: Execution Model ) and default input-output. The procedure print used in the first version of our "Hello World" comes from ANY.
NONE inherits from any class that has no explicit heir. Since inheritance has no cycles, NONE cannot have proper descendants. This makes it useful, as we will see, to specify non-exported features, and to denote the type of void values. Unlike ANY, class NONE doesn't have an actual class text; instead, it's a convenient fiction.
Classes are the only form of module in Eiffel. As will be explained in more detail, they also provide the basis for the only form of type. This module-type identification is at the heart of object technology and of the fundamental simplicity of the Eiffel method.
Above classes, you will find the concept of cluster. A cluster is a group of related classes. Clusters are a property of the method, enabling managers to organize the development into teams. As we have already seen (in The Software Process in Eiffel ) they also play a central role in the lifecycle model. Clusters are an organizational concept, not a form of module, and do not require an Eiffel programming language construct.
The subsequent sections will show how to write Eiffel classes with their features. In an Eiffel system, however, not everything has to be written in Eiffel: some features may be external , coming from languages such as C, C++, Java, C# Fortran and others. For example a feature declaration may appear (in lieu of the forms seen later) as
to indicate that it is actually an encapsulation of a C function whose original name is _fstat. The alias clause is optional, but here it is needed because the C name, starting with an underscore, is not valid as an Eiffel identifier.
Similar syntax exists to interface with C++ classes. EiffelStudio includes a tool called Legacy++ which will automatically produce, from a C++ class, an Eiffel class that encapsulates its facilities, making them available to the rest of the Eiffel software as bona fide Eiffel features.
These mechanisms illustrate one of the roles of Eiffel: as an system architecturing and software composition tool, used at the highest level to produce systems with robust, flexible structures ready for extendibility, reusability and maintainability. In these structures not everything must be written in the Eiffel programming language: existing software elements and library components can play their part, with the structuring capabilities of Eiffel (classes, information hiding, inheritance, clusters, contracts and other techniques seen in this presentation) serving as the overall wrapping mechanism.
A system with a certain static structure describes a set of possible executions. The run-time model governs the structure of the data (objects) created during such executions.
The properties of the run-time model are not just of interest to implementers; they also involve concepts directly relevant to the needs of system modelers and analysts at the most abstract levels.
A class was defined as the static description of a type of run-time data structures. The data structures described by a ca class are called instances of the class, which in turn is called their generating class (or just "generator"). An instance of ACCOUNT is a data structure representing a bank account; an instance of LINKED_LIST is a data structure representing a linked list.
An object, as may be created during the execution of a system, is an instance of some class of the system.
Classes and objects belong to different worlds: a class is an element of the software text; an object is a data structure created during execution. Although is possible to define a class whose instances represent classes, this does not eliminate the distinction between a static, compile-time notion, class, and a dynamic, run-time notion, object.
An object is either an atomic object (integer, real, boolean, double) or a composite object made of a number of fields, represented by adjacent rectangles on the conventional run-time diagrams:
Each field is a value. A value can be either an object or an object reference:
A feature, as noted, is an operation available on instances of a class. A feature can be either an attribute or a routine. This classification, which you can follow by starting from the right on the figure above, is based on implementation considerations:
ACCOUNT may have an attribute balance; then all instances of the class will have a corresponding field containing each account's current balance.
ACCOUNT may have a routine withdraw .
withdraw will be a procedure; an example of function may be highest_deposit, which returns the highest deposit made so far to the account.
If we instead take the viewpoint of the clients of a class (the classes relying on its feature), you can see the relevant classification by starting from the left on the figure:
balance; a query with arguments, such as balance_on (d) , returning the balance at date d, can only be a function.
From the outside, there is no difference between a query implemented as an attribute and one implemented as a function: to obtain the balance of an account a, you will always write a.balance. In the implementation suggested above, a is an attribute, so that the notation denotes an access to the corresponding object field. But it is also possible to implement a as a function, whose algorithm will explore the lists of deposits and withdrawals and compute their accumulated value. To the clients of the class, and in the official class documentation as produced by the environment tools, the difference is not visible.
This principle of Uniform Access is central to Eiffel's goals of extendibility, reusability and maintainability: you can change the implementation without affecting clients; and you can reuse a class without having to know the details of its features' implementations. Most object-oriented languages force clients to use a different notation for a function call and an attribute access. This violates Uniform Access and is an impediment to software evolution, turning internal representation changes into interface changes that may disrupt large parts of a system.
The following simple class text illustrates the preceding concepts
note description: "Simple bank accounts" class ACCOUNT feature -- Access balance: INTEGER -- Current balance deposit_count: INTEGER -- Number of deposits made since opening do if all_deposits /= Void then Result := all_deposits.count end end feature -- Element change deposit (sum: INTEGER) -- Add `sum' to account. do if all_deposits = Void then create all_deposits end all_deposits.extend (sum) balance := balance + sum end feature {NONE} -- Implementation all_deposits: DEPOSIT_LIST -- List of deposits since account's opening. invariant consistent_balance: (all_deposits /= Void) implies (balance = all_deposits.total) zero_if_no_deposits: (all_deposits = Void) implies (balance = 0) end -- class ACCOUNT
(The {NONE} qualifier and the invariant clause, used here to make the example closer to a real class, will be explained shortly. DEPOSIT_LIST refers to another class, which can be written separately using library classes.)
It's easy to deduce, from a feature's syntactic appearance, the category to which it belongs. Here:
deposit and deposit_count, which include a do ... clause, are routines.
balance and all_deposits, which are simply declared with a type, are attributes. Note that even for attributes it is recommended to have a header comment.
deposit_count is declared as returning a result (of type INTEGER); so it is a function.
deposit has no such result and hence is a procedure.
Classes, as noted, are a static notion. Objects appear at run time; they are created explicitly. Here is the basic instruction to create an object of type ACCOUNT and attach it to x:
create xassuming that x has been declared of type ACCOUNT. Such an instruction must be in a routine of some class -- the only place where instructions can appear -- and its effect at run time will be threefold: create a new object of type ACCOUNT; initialize its fields to default values; and attach the value of x to it. Here the object will have two fields corresponding to the two attributes of the generating class: an integer for balance, which will be initialized to 0, and a reference for all_deposits, which will be initialized to a void reference:
The language specifies default initialization values for all possible types:
|
Type |
Default value | |
|
Zero | ||
|
False | ||
|
Null | ||
|
Reference types (such as ACCOUNT and |
Void reference | |
|
Composite expanded types (see next) |
Same rules, applied recursively to all fields |
It is possible to override the initialization values by providing -- as in the earlier example of class HELLO -- one or more creation procedures. For example we might change ACCOUNT to make sure that every account is created with an initial deposit:
note description : "Simple bank accounts, initialized with a first deposit" class ACCOUNT1 create make feature -- Initialization make (sum: INTEGER) -- Initialize account with `sum' . do deposit (sum) end -- The rest of the class as for ACCOUNT end -- class ACCOUNT1
A create clause may list zero or more (here just one) procedures of the class.
Info: Note the use of the same keyword, create , for both a creation clause, as here, and creation instructions such as create x .
In this case the original form of creation instruction, create x , is not valid any more for creating an instance of ACCOUNT1; you must use the form
create x.make (2000)
known as a creation call. Such a creation call will have the same effect as the original form -- creation, initialization, attachment to -- x followed by the effect of calling the selected creation procedure, which here will call deposit with the given argument.
Note that in this example all that make does is to call deposit. So an alternative to introducing a new procedure make would have been simply to introduce a creation clause of the form create deposit , elevating deposit to the status of creation procedure. Then a creation call would be of the form create x.deposit (2000) .
Info: Some variants of the basic creation instruction will be reviewed later: instruction with an explicit type; creation expressions. See "Creation variants" .
The example assumed x declared of type ACCOUNT (or ACCOUNT1). Such an x is an example of entity, a notion generalizing the well-known concept of variable. An entity is a name that appears in a class text to represent possible run-time values (a value being, as defined earlier, an object or a reference). An entity is one of the following:
balance and all_deposits.
sum for deposit and make.
Result in a function.
The third case, local entities, arises when a routine needs some auxiliary values for its computation. Here is an example of the syntax:
deposit (sum: INTEGER) -- Add sum to account. local new: AMOUNT do create new.make (sum) all_deposits.extend (new) balance := balance + sum end
This example is a variant of deposit for which we assume that the elements of a DEPOSIT_LIST such as all_deposits are no longer just integers, but objects, instances of a new class, AMOUNT. Such an object will contain an integer value, but possibly other information as well. So for the purpose of procedure deposit we create an instance of AMOUNT and insert it, using procedure extend, into the list all_deposits. The object is identified through the local entity new, which is only needed within each execution of the routine (as opposed to an attribute, which yields an object field that will remain in existence for as long as the object).
The last case of entity, Result, serves to denote, within the body of a function, the final result to be returned by that function. This was illustrated by the function deposit_count, which read
deposit_count: INTEGER -- Number of deposits made since opening (provisional version) do if all_deposits /= Void then Result := all_deposits.count end end
The value returned by any call will be the value of the expression all_deposits.count (to be explained in detail shortly) for that call, unless all_deposits is a Void reference ( /= means "not equal").
The default initialization rules seen earlier for attributes (see the table above) also serve to initialize local entities and Result on routine entry. So in the last example, if all_deposits is void (as in the case on initialization with the class as given so far), Result keeps its default value of 0, which will be returned as the result of the function.
Apart from object creation, the basic computational mechanism, in the object-oriented style of computation represented by Eiffel, is feature call. In its basic form, it appears as
target.feature (argument1, ...)
where target is an entity or more generally an expression, feature is a feature name, and there may be zero or more argument expressions. In the absence of any argument the part in parentheses should be removed.
We have already seen such calls. If the feature denotes a procedure, the call is an instruction, as in
all_deposits.extend (new)
If feature denotes a query (function or attribute), the call is an expression, as in the right-hand side of
Result := all_deposits.count
Following the principle of Uniform Access (mentioned earlier in the section Objects, fields, values, and references), this form is the same for calls to attributes and to functions without arguments. In this example, feature count from class DEPOSIT_LIST may indeed be implemented in either of these two ways: we can keep a count field in each list, updating it for each insertion and removal; or we can compute count, whenever requested, by traversing the list and counting the number of items.
In the case of a routine with arguments -- procedure or function -- the routine will be declared, in its class, as
some_feature (formal_1: TYPE_1; ...) do ... end
meaning that, at the time of each call, the value of each formal will be set to the corresponding actual (formal_1 to argument_1 and so on).
In the routine body, it is not permitted to change the value of a formal argument, although it is possible to change the value of an attached object through a procedure call such as formal_1.some_procedure ( ... ) .
Basic types such as INTEGER are, as noted, full-status citizens of Eiffel's type system, and so are declared as classes (part of the Kernel Library). INTEGER, for example, is characterized by the features describing integer operations: plus, minus, times, division, less than, and so on.
With the dot notation seen so far, this would imply that simple arithmetic operations would have to be written with a syntax such as
i.plus (j)
i + jINTEGER as
Such a feature has all the properties and prerogatives of both normal "identifier-dot" notation and infix notation. This allowing invoking plus using either notation: i.plus (j) or i + j . A feature such as plus allowing infix notation must be a function, and take exactly one argument.
Prefix notation is allowed as well. A function can be declared as opposite alias "-" , with no argument, permitting calls of the form -3 rather than (3).opposite .
Predefined library classes covering basic types such as INTEGER, CHARACTER, BOOLEAN, REAL, DOUBLE are known to the Eiffel compiler, so that a call of the form j + i, although conceptually equivalent to a routine call, can be processed just as efficiently as the corresponding arithmetic expression in an ordinary programming language. This brings the best of both worlds: conceptual simplicity, enabling Eiffel developers, when they want to, to think of integers and the like as objects; and efficiency as good as in lower-level approaches.
Infix and prefix notations are available to any class, not just the basic types' predefined classes. For example a graphics class could use the name distance alias "|-|" for a function computing the distance between two points, to be used in expressions such as
point1 |-| point2
Every entity appearing in an Eiffel text is declared as being of a certain type, using the syntax already encountered in the above examples:
entity_name: TYPE_NAMEThis applies to attributes, formal arguments of routines and local entities. You will also declare the result type for a function, as in the earlier example
deposit_count: INTEGER ...
Specifying such a function result type also declares, implicitly, the type for Result as used in the function's body.
What is a type? With the elements seen so far, every type is a class . INTEGER, used in the declaration of deposits_count, is, as we have seen, a library class; and the declaration all_deposits: DEPOSIT_LIST assumes the existence of a class DEPOSIT_LIST .
Three mechanisms introduced below -- expanded types, genericity, and anchored declarations -- will generalize the notion of type slightly. But they do not change the fundamental property that every type is based on a class, called the type's base class. In the examples seen so far, each type is a class, serving as its own base class.
An instance of a class C is also called "an object of type C".
It was noted above that a value is either an object or a reference. This corresponds to two kinds of type: reference types and expanded types.
If a class is declared as just
class CLASS_NAME ...it defines a reference type. The entities declared of that type will denote references. So in the declaration
x: ACCOUNTthe possible run-time values for x are references, which will be either void or attached to instances of class ACCOUNT .
Instead of class, however, you may use the double keyword expanded class , as in the EiffelBase class definition
In this case the value of an entity declared as n: INTEGER is not a reference to an object, but the object itself -- in this case an atomic object, an integer value.
Expanded classes make it possible to construct composite objects with subobjects. Suppose that two classes, ENGINE and PLANT, are suppliers to the class CAR. Further, ENGINE is defined as expanded, and PLANT is not defined as expanded. So, here's an abbreviated class declaration (note clause and routines omitted) for CAR:
class CAR feature engine: ENGINE originating_plant: PLANT end -- class CAR
We can illustrate the structure of a typical instance of CAR like this:
The field for the attribute originating_plant is a reference to an object of type PLANT external to the instance of CAR. But in the case of the attribute engine, the fields for the instance of ENGINE exist as a subobject within the instance of CAR, because of class ENGINE's expanded nature.
This example also illustrates that the distinction between expanded and reference types is important not just for system implementation purposes but for high-level system modeling as well. Consider the example of a class covering the notion of car. Many cars share the same originating_plant, but an engine belongs to just one car. References represent the modeling relation "knows about"; subobjects, as permitted by expanded types, represent the relation "has part", also known as aggregation. The key difference is that sharing is possible in the former case but not in the latter.
To assign, copy and compare values, you can rely on a number of mechanisms. Two of them, assignment and equality testing, are language constructs; the others are library features, coming from the top-level class ANY seen earlier.
Assignment uses the symbol := . The assignment instruction
x := yupdates the value of x to be the same as that of y. This means that for entities of reference types, the value of x will be a void reference if the value of y is void, and otherwise x will be attached to the same object OBJ2 as y:
For entities of expanded types, the values are objects; the object attached to x will be overwritten with the contents of the object attached to y. In the case of atomic objects, as in n := 3 with the declaration n: INTEGER , this has the expected effect of assigning to n the integer value 3; in the case of composite objects, this overwrites the fields for x, one by one, with the corresponding y fields.
To copy an object, use
x.copy (y)
x and y are non-void, and copies the contents of y's attached object onto those of x's. For expanded entities the effect is the same as that of the assignment x := y.
An operation performing similar duty to copy is twin . The assignment
x := y.twin
y is non-void), initialized with a copy of the object attached to y and attaches the result to x . This means we may view twin as a function that performs the following two steps:
create Result Result.copy (Current)
y to which the twin call is targeted.
So, assuming both entities of reference types and y not void, the assignment above will attach x to a new object identical to y's attached object, as opposed to the assignment x := y which attaches x to the same object as y.
To determine whether two values are equal, use the expression:
x = y /= , as in:
x /= yAs with assignment, there is also a form that works on objects rather than references:
x.is_equal (y)
x and y are both non-void and attached to field-by-field identical objects. This can be true even when x = y is not, for example, in the figure, before the assignment, if the two objects shown are field-by-field equal.
The expression x.is_equal (y) can be written alternatively, using the tilde ('~') character, in a notation similar in form to x = y . The expression:
x ~ y
will be true only in cases in which x and y are the same type and x.is_equal (y) is true.
A more general variant of is_equal is used under the form:
equal (x, y)
x is void, returning true whenever is_equal would but also if x and y are both void. (In contrast, x.is_equal (y) is not defined for void x and would, if evaluated, yield an exception as explained in "Exception handling" below.)
Note: The ~ operator performs an object equality comparison, using the (possibly redefined) version of feature is_equal that is appropriate for the operand types. The operand types must be the same, or the result will be False. As such, the use of ~ is preferred to over the use of direct use of either x.is_equal (y) or equal (x, y), which can be susceptible to catcalls.
Void denotes a void reference. So you can make x void through the assignment
x := Void
if x = Void then ...
Note that the assignment, := , and the equality operators, =, ~, /~, and /= , are language constructions, whereas copy, twin, is_equal, and equal are library features coming from class ANY .
Void is a language keyword with built-in characteristics, but it is not harmful to imagine Void as another feature declared in class ANY, with type of NONE, the "bottom" type. This convenience allows any assignment of the for x := Void to be valid without any making exceptions to the type rules, regardless of the type of x .
Using the redefinition mechanisms to be seen in the discussion of inheritance, a class can redefine copy and is_equal to cover specific notions of copy and equality. The assertions will ensure that the two remain compatible: after x.copy (y) , the property x .is_equal (y) must always be true. The effect of twin will automatically follow a redefinition of copy, and equal will follow is_equal.
To guarantee the original, non-redefined semantics you may use the variants standard_copy, standard_twin, standard_equal, all defined in ANY as "frozen", that is to say non-redefinable.
Feature twin only duplicates one object. If some of the object's fields are references to other objects, the references themselves will be copied, not those other objects.
It is useful, in some cases, to duplicate not just one object but an entire object structure. The expression y.deep_twin achieves this goal: assuming non-void y, it will produce a duplicate not just of the object attached to y but of the entire object structure starting at that object. The mechanism respects all the possible details of that structure, such as cyclic reference chains. Like the preceding features, deep_twin comes from class ANY.
A related mechanism provides a powerful persistence facility. A call of the form
x.store (Some_file_or_network_connection)
will store a copy of the entire object structure starting at x , under a suitable representation. Like deep_twin, procedure store will follow all references to the end and maintain the properties of the structure. The function retrieved can then be used -- in the same system, or another -- to recreate the structure from the stored version.
As the name suggests, Some_file_or_network_connection can be an external medium of various possible kinds, not just a file but possibly a database or network. The EiffelNet client-server library indeed uses the store - retrieved mechanism to exchange object structures over a network, between compatible or different machine architectures, for example a Windows client and a Unix server.
Reference reattachments, x := y , of the form illustrated by the figure just above can cause objects to become unreachable. This is the case for the object identified as OBJ1 on that figure (the object to which x was attached before the assignment) if no other reference was attached to it.
In all but toy systems, it is essential to reclaim the memory that has been allocated for such objects; otherwise memory usage could grow forever, as a result of creation instructions create x ... and calls to twin and the like, leading to thrashing and eventually to catastrophic termination.
The Eiffel method suggests that the task of detecting and reclaiming such unused object space should be handled by an automatic mechanism (part of the Eiffel run-time environment), not manually by developers (through calls to procedures such as Pascal's dispose and C/C++'s free). The arguments for this view are:
Simplicity : handling memory reclamation manually can add enormous complication to the software, especially when -- as is often the case in object-oriented development -- the system manipulates complex run-time data structures with many links and cycles.
Reliability : memory management errors, such as the incorrect reclamation of an object that is still referenced by a distant part of the structure, are a notorious source of dangerous and hard-to-correct bugs.
The Eiffel Software's implementation of Eiffel provides a sophisticated garbage collector which efficiently handles the automatic reclamation process, while causing no visible degradation of a system's performance and response time.
The basic form of computation, it has been noted, is a call of the form target.feature (...) . This is only meaningful if feature denotes a feature of the generating class of the object to which target (assumed to be non-void) is attached. The precise rule is the following:
Rule -- Feature Call: A call of the form target.feature (...) appearing in a class C is only valid if feature is a feature of the base class of target's type, and is available to C.
The first condition simply expresses that if target has been declared as target: A then feature must be the name of one of the features of A. The second condition reflects Eiffel's application of the principles of information hiding. A feature clause, introducing one or more feature declarations, may appear not only as
feature -- Comment identifying the feature category ... Feature declaration ... ... Feature declaration ... ...
but may also include a list of classes in braces, feature {A, B, ... } , as was illustrated for ACCOUNT:
feature {NONE} -- Implementation all_deposits: DEPOSIT_LIST -- List of deposits since account's opening.
This form indicates that the features appearing in that clause are only available -- in the sense of available for calls, as used in the Feature Call rule -- to the classes listed. In the example feature all_deposits is only available to NONE . Because of the global inheritance structure, this means it is in fact available to no useful client at all, and is equivalent in practice to feature { } with an empty class list, although the form listing NONE explicitly is more visible and hence preferred.
With this specification a class text including the declaration acc: ACCOUNT and a call of the form
acc.all_depositsviolates the Feature Call rule and will be rejected by the EiffelStudio compiler.
Besides fully exported features (introduced by feature ... ; without further qualification) and fully secret ones (feature { } or feature {NONE} ), it is possible to export features selectively to some specified classes, using the specification
feature {A, B, ...}
for arbitrary classes A, B, ... This enables a group of related classes to provide each other with privileged access, without requiring the introduction of a special module category above the class level (see "Clusters" ).
Exporting features selectively to a set of classes A, B, ... also makes them available to the descendants of these classes. So a feature clause beginning with just feature is equivalent to one starting with feature {ANY} .
These rules enable successive feature clauses to specify exports to different clients. In addition, the recommended style, illustrated in the examples of this chapter, suggests writing separate feature clauses -- regardless of their use for specifying export privileges -- to group features into separate categories. The standard style rules define a number of fundamental categories and the order in which they should appear; they include: Initialization for creation procedures, Access for general queries, Status report for boolean-valued queries, Status setting, Element change, Implementation (for selectively exported or secret features. Every feature in the EiffelBase library classes belongs to one of the predefined categories.
The Feature Call rule is the first of the rules that make Eiffel a statically typed approach, where the applicability of operations to objects is verified at compile time rather than during execution. Static typing is one of the principal components of Eiffel's support for reliability in software development.
The preceding elements make it possible to understand the overall scheme of an Eiffel system's execution.
At any time during the execution of a system, one object is the current object of the execution, and one of the routines of the system, the current routine, is being executed, with the current object as its target. (We will see below how the current object and current routine are determined.) The text of a class, in particular its routines, make constant implicit references to the current object. For example in the instruction
balance := balance + sum
appearing in the body of procedure deposit of class ACCOUNT, the name of the attribute balance, in both occurrences, denotes the balance field of the current object, assumed to be an instance of ACCOUNT. In the same way, the procedure body that we used for the creation procedure make in the ACCOUNT1 variant
make (sum: INTEGER) -- Initialize account with sum . do deposit (sum) end
contains a call to the procedure deposit. Contrary to earlier calls written in dot notation as target.feature (...), the call to deposit has no explicit target; this means its target is the current object, an instance of ACCOUNT1. Such a call is said to be unqualified; those using dot notations are qualified calls.
Although most uses of the current object are implicit, a class may need to name it explicitly. The predefined expression Current is available for that purpose. A typical use, in a routine merge (other: ACCOUNT ) of class ACCOUNT, would be a test of the form
if other = Current then report_error ("Error: trying to merge an account with itself!") else ... Normal processing (merging two different account) ... end
With these notions it is not hard to define precisely the overall scenario of a system execution by defining which object and routine will, at each instant, be the current object and the current routine:
Starting a system execution, as we have seen, consists in creating an instance of the root class, the root object, and executing a designated creation procedure, the root procedure, with the root object as its target. The root object is the initial current object, and the root procedure is the initial current procedure.
From then on only two events can change the current object and current procedure: a qualified routine call; and the termination of a routine.
In a call of the form target.routine (...) , target denotes a certain object TC. (If not, that is to say, if the value of target is void, attempting to execute the call will trigger an exception, as studied below.) The generating class of TC must, as per the Feature Call rule, contain a routine of name routine. As the call starts, TC becomes the new current object and routine becomes the new current routine.
When a routine execution terminates, the target object and routine of the most recent non-terminated call -- which, just before the terminated call, were the current object and the current routine -- assume again the role of current object and current routine.
The only exception to the last rule is termination of the original root procedure call; in this case the entire execution terminates.
The description of assignments stated that in x := y the target x must be an entity. More precisely it must be a writable entity. This notion excludes formal routine arguments: as noted, a routine r (arg: SOME_TYPE) may not assign to arg (reattaching it to a different object), although it can change the attached objects through calls of the form arg.procedure (...) .
Allowing only entities to be the targets of assignments precludes assignments of the form
obj.some_attribute := some_value -- This syntax is disallowed (except in the presence of an `assigner command', see below)
obj.some_attribute is an expression (a feature call), not an entity: you may no more assign to obj.some_attribute than to, say, b + a -- another expression that is also, formally, a feature call.
To obtain the intended effect of such an assignment you may use a procedure call, where the base class of obj's type has defined the procedure
set_some_attribute (v: VALUE_TYPE) -- Set value of some_attribute to `v'. do some_attribute := v end
So instead of the disallowed assignment shown above, you would code:
obj.set_some_attribute (some_value)
This rule is essential to enforcing the method. Permitting direct assignments to an object's fields -- as in C++ and Java -- would violate all the tenets of information hiding by letting clients circumvent the interface carefully crafted by the author of a supplier class.
However, many developers have become accustomed to reading and writing code in other languages which do allow assignments of the form:
obj.some_attribute := some_value
some_attribute to be declared something like this:
some_attribute: SOME_TYPEsome_attribute: SOME_TYPE assign set_some_attribute
set_some_attribute coded as shown in the previous section. In the presence of the assigner command, the previously invalid assignment syntax is now valid. But it is translated by the compiler as a call to set_some_attribute, using the source of the assignment as an argument.
It is the responsibility of each class author to define the exact privileges that the class gives to each of its clients, in particular field modification rights. Building a class is like building a machine: you design the internals, to give yourself the appropriate mechanisms; and you design the control panel, letting users (clients) access the desired subset of these mechanisms, safely and conveniently.
The levels of privilege available to the class author include, for any field:
NONE.
set_attribute kind.
deposit of class ACCOUNT, which adds a specified amount to the balance field, rather than directly setting the balance.
The last case is particularly interesting is that it allows the class designer to set the precise way in which clients will manipulate the class instances, respecting the properties of the class and its integrity. The exported routines may, through the Design by Contract mechanism reviewed later in ( ET: Design by Contract (tm), Assertions and Exceptions ), place some further restrictions on the permitted modifications, for example by requiring the withdrawn amount to be positive.
These rules follow directly from the more general goals (reusability, extendibility, reliability) and principles (Uniform Access, information hiding) underlying Eiffel software design. They reflect a view that each class must denote a well-understood abstraction, defined by a set of exported features chosen by the class designer -- the "control panel".
The class documentation (see the contract form of a class ) makes this view clear to client authors; no violation of that interface is permitted. This approach also paves the way for future generalization -- the final step of the cluster lifecycle, seen earlier in the section Generalization and reuse -- of the most promising components, and their inclusion into reusable libraries.
In certain situations it is beneficial to be able to declare class attributes which behave in specialized ways.
Part of the strategy to ensure void-safety makes it necessary to be able to declare attributes as either detachable or attached.
Self-initializing attributes and stable attributes are other tools for making void-safe programming more convenient.
These attribute specializations are presented in the void-safe programming chapter.
Another special type of attribute supported by Eiffel Software's compiler is the transient attribute. When an instance of a class to which a transient attribute belongs is saved to persistent storage, the field for the transient attribute is not included. So, transient attributes are transient in the sense that they are part of the object at runtime, but not when the object is stored on disk.
This type of attribute has benefits when using the persistence mechanisms provided with EiffelStudio, like SED. Because transient attributes are not stored, they need not be accounted for upon retrieval. So, objects stored before changes to a class that only affect transient attributes will still be retrievable using the new class definition (whereas, if non-transient attributes were changed, a mismatch would occur during retrieval).
An attribute is marked as transient by including a note option in its declaration:
transient_attribute: detachable STRING note option: transient attribute end
Only certain attributes can be marked as transient. Specifically, if attribute a is declared of type T, it can be marked as transient only if it satisfies the following conditions:
The EiffelBase class INTERNAL includes features which are used to distinguish object fields as either persistent or transient and to reveal how many transient fields an object has.
Note: Prior to version 6.6, support for transient attributes was limited to the C storable mechanism. In version 6.6, support was added for the Eiffel storable mechanism (SED) on both classic and .NET system targets.
Some of the classes that we will need, particularly in libraries, are container classes, describing data structures made of a number of objects of the same or similar types. Examples of containers include arrays, stacks and lists. The class DEPOSIT_LIST posited in earlier examples describes containers.
It is not hard, with the mechanisms seen so far, to write the class DEPOSIT_LIST, which would include such features as count (query returning the number of deposit objects in the list) and put (command to insert a new deposit object).
Most of the operations, however, would be the same for lists of objects other than deposits. To avoid undue replication of efforts and promote reuse, we need a way to describe generic container classes, which we can use to describe containers containing elements of many different types.
The notation
class C [G] ... The rest as for any other class declaration ...
introduces a generic class. A name such as G appearing in brackets after the class name is known as a formal generic parameter; it represents an arbitrary type.
Within the class text, feature declarations can freely use G even though it is not known what type G stands for. Class LIST of EiffelBase, for example, includes features
first: G -- Value of first list item extend (val: G) -- Add a new item of value val at end of list ...
The operations available on an entity such as first and val, whose type is a formal generic parameter, are the operations available on all types: use as source y of an assignment x := y, use as target x of such an assignment (although not for val, which as a formal routine argument is not writable), use in equality comparisons x = y or x /= y, and application of universal features from ANY such as twin, is_equal and copy.
To use a generic class such as list, a client will provide a type name as actual generic parameter. So instead of relying on a special purpose class DEPOSIT_LIST, the class ACCOUNT could include the declaration
all_deposits: LIST [DEPOSIT]
using LIST as a generic class and DEPOSIT as the actual generic parameter. Then all features declared in LIST as working on values of type G will work, when called on the target all_deposits, on values of type DEPOSIT. With the target
all_accounts: LIST [ACCOUNT]
these features would work on values of type ACCOUNT.
Info: A note of terminology: to avoid confusion, Eiffel always uses the word argument for routine arguments, reserving parameter for the generic parameters of classes.
Genericity reconciles extendibility and reusability with the static type checking demanded by reliability. A typical error, such as confusing an account and a deposit, will be detected immediately at compile time, since the call all_accounts. extend ( dep ) is invalid for dep declared of type DEPOSIT. What is valid is something like all_accounts. extend ( acc ) for acc of type ACCOUNT. In other approaches, the same effect might require costly run-time checks (as in Java, C# or Smalltalk), with the risk of run-time errors.
Info: This form of genericity is known as unconstrained because the formal generic parameter, G in the example, represents an arbitrary type. You may also want to use types that are guaranteed to have certain operations available. This is known as constrained genericity and will be studied with inheritance.
An example of generic class from the Kernel Library is ARRAY [G], which describes direct-access arrays. Features include:
put to replace an element's value, as in my_array.put (val, 25) which replaces by val the value of the array entry at index 25.
item to access an entry, as in my_array.item (25) yielding the entry at index 25. A synonym is infix "@", so that you may also write more tersely, for the same result, my_array @ 25 .
lower, upper and count: queries yielding the bounds and the number of entries.
make, as in create my_array.make (1, 50) which creates an array with the given index bounds. It is also possible to resize an array through resize, retaining the old elements. In general, the Eiffel method abhors built-in limits, favoring instead structures that resize themselves when needed, either from explicit client request or automatically.
The comment made about INTEGER and other basic classes applies to ARRAY too: Eiffel compilers know about this class, and will be able to process expressions of the form my_array.put (val, 25) and my_array @ 25 in essentially the same way as a C or Fortran array access -- my_array [25] in C. But it is consistent and practical to let developers treat ARRAY as a class and arrays as objects; many library classes in EiffelBase, for example, inherit from ARRAY. Once again the idea is to get the best of both worlds: the convenience and uniformity of the object-oriented way of thinking; and the efficiency of traditional approaches.
A similar technique applies to another Kernel Library class, that one not generic: STRING, describing character strings with a rich set of string manipulation features.
The introduction of genericity brings up a small difference between classes and types. A generic class C is not directly a type since you cannot declare an entity as being of type C: you must use some actual generic parameter T -- itself a type. C [T] is indeed a type, but class C by itself is only a type template.
The process of obtaining a type C [T] from a general class C is known as a generic derivation; C [T] is a generically derived type. Type T itself is, recursively, either a non-generic class or again a generically derived type D [U] for some D and U, as in LIST [ARRAY [INTEGER]].)
It remains true, however, that every type is based on a class. The base class of a generically derived type C [T] is C.
Eiffel directly implements the ideas of Design by Contract , which enhance software reliability and provide a sound basis for software specification, documentation and testing, as well as exception handling and the proper use of inheritance.
Contents
|
A system -- a software system in particular, but the ideas are more general -- is made of a number of cooperating components. Design by Contract states that their cooperation should be based on precise specifications -- contracts -- describing each party's expectations and guarantees.
An Eiffel contract is similar to a real-life contract between two people or two companies, which it is convenient to express in the form of tables listing the expectations and guarantees. Here for example is how we could sketch the contract between a homeowner and the telephone company:
| provide telephone service | OBLIGATIONS | BENEFITS |
| Client | (Satisfy precondition:) Pay bill. | (From postcondition:) Receive telephone service from Supplier. |
| Supplier | (Satisfy precondition:) Provide telephone service. | (From postcondition:) No need to provide anything if bill not paid. |
Note how the obligation for each of the parties maps onto a benefit for the other. This will be a general pattern.
The client's obligation, which protects the supplier, is called a precondition. It states what the client must satisfy before requesting a certain service. The client's benefit, which describes what the supplier must do (assuming the precondition was satisfied), is called a postcondition.
In addition to preconditions and postconditions, contract clauses include class invariants, which apply to a class as a whole. More precisely a class invariant must be ensured by every creation procedure (or by the default initialization if there is no creation procedure), and maintained by every exported routine of the class.
Eiffel provides syntax for expressing preconditions (require), postconditions (ensure) and class invariants (invariant), as well as other assertion constructs studied later (see "Instructions" ): loop invariants and variants, check instructions.
Here is a partial update of class ACCOUNT with more assertions:
note description: "Simple bank accounts" class ACCOUNT feature -- Access balance: INTEGER -- Current balance deposit_count: INTEGER -- Number of deposits made since opening do ... As before ... end feature -- Element change deposit (sum: INTEGER) -- Add `sum' to account. require non_negative: sum >= 0 do ... As before ... ensure one_more_deposit: deposit_count = old deposit_count + 1 updated: balance = old balance + sum end feature {NONE} -- Implementation all_deposits: DEPOSIT_LIST -- List of deposits since account's opening. invariant consistent_balance: (all_deposits /= Void) implies (balance = all_deposits . total) zero_if_no_deposits: (all_deposits = Void) implies (balance = 0) end -- class ACCOUNT
Each assertion is made of one or more subclauses, each of them a boolean expression (with the additional possibility of the old construct). The effect of including more than one sub clause, as in the postcondition of deposit and in the invariant, is the same as connecting them through an and. Each clause may be preceded by a label, such as consistent_balance in the invariant, and a colon; the label is optional and does not affect the assertion's semantics, except for error reporting as explained in the next section, but including it systematically is part of the recommended style. The value of the boolean expression a implies b is true except if a is true and b false.
Because assertions benefit from the full power of boolean expressions, they may include function calls. This makes it possible to express sophisticated consistency conditions, such as " the graph contains no cycle", which would not be otherwise expressible through simple expressions, or even through first-order predicate calculus, but which are easy to implement as Eiffel functions returning boolean results.
The precondition of a routine expresses conditions that the routine is imposing on its clients. Here a call to deposit is correct if and only if the value of the argument is non-negative. The routine does not guarantee anything for a call that does not satisfy the precondition. It is in fact part of the Eiffel method that a routine body should never test for the precondition, since it is the client's responsibility to ensure it. (An apparent paradox of Design by Contract, which is reflected in the bottom-right entries of the preceding and following contract tables, and should not be a paradox any more at the end of this discussion, is that one can get more reliable software by having fewer explicit checks in the software text.)
The postcondition of a routine expresses what the routine guaranteed to its clients for calls satisfying the precondition. The notation old expression, valid in postconditions ( ensure clauses) only, denotes the value that expression had on entry to the routine.
The precondition and postcondition state the terms of the contract between the routine and its clients, similar to the earlier example of a human contract:
deposit
| OBLIGATIONS | BENEFITS |
| Client | (Satisfy precondition:) Use a non-negative argument. | (From postcondition:) Get deposits list and balance updated. |
| Supplier | (Satisfy precondition:) Update deposits list and balance. | (From postcondition:) No need to handle negative arguments. |
The class invariant, as noted, applies to all features. It must be satisfied on exit by any creation procedure, and is implicitly added to both the precondition and postcondition of every exported routine. In this respect it is both good news and bad news for the routine implementer: good news because it guarantees that the object will initially be in a stable state, averting the need in the example to check that the total of all_deposits is compatible with the balance; bad news because, in addition to its official contract as expressed by its specific postcondition, every routine must take care of restoring the invariant on exit.
A requirement on meaningful contracts is that they should be in good faith: satisfiable by an honest partner. This implies a consistency rule: if a routine is exported to a client (either generally or selectively), any feature appearing in its precondition must also be available to that client. Otherwise -- for example if the precondition included require n > 0, where n is a secret attribute -- the supplier would be making demands that a good-faith client cannot possibly check for.
Note in this respect that guaranteeing a precondition does not necessarily mean, for the client, testing for it. Assuming n is exported, a call may test for the precondition
if x.n > 0 then x.r end
possibly with an else part. But if the context of the call, in the client's code, implies that n is positive -- perhaps because some preceding call set it to the sum of two squares -- then there is no need for an if or similar construct.
Note: In such a case, a check instruction as introduced later ( "Instructions" ) is recommended if the reason for omitting the test is non-trivial.
What are contracts good for? Their first use is purely methodological. By applying a discipline of expressing, as precisely as possible, the logical assumptions behind software elements, you can write software whose reliability is built-in: software that is developed hand-in-hand with the rationale for its correctness.
This simple observation -- usually not clear to people until they have practiced Design by Contract thoroughly on a large-scale project -- brings as much change to software practices and quality as the rest of object technology.
Contracts in Eiffel are not just wishful thinking. They can be monitored at run time under the control of compilation options.
It should be clear from the preceding discussion that contracts are not a mechanism to test for special conditions, for example erroneous user input. For that purpose, the usual control structures ( if deposit_sum > 0 then ...) are available, complemented in applicable cases by the exception handling mechanism reviewed next. An assertion is instead a correctness condition governing the relationship between two software modules (not a software module and a human, or a software module and an external device). If sum is negative on entry to deposit, violating the precondition, the culprit is some other software element, whose author was not careful enough to observe the terms of the deal. Bluntly:
Rule -- Assertion Violation: A run-time assertion violation is the manifestation of a bug.
To be more precise:
That violations indicate bugs explains why it is legitimate to enable or disable assertion monitoring through mere compilation options: for a correct system -- one without bugs -- assertions will always hold, so the compilation option makes no difference to the semantics of the system.
But of course for an incorrect system the best way to find out where the bug is -- or just that there is a bug -- is often to monitor the assertions during development and testing. Hence the presence of the compilation options, which EiffelStudio lets you set separately for each class, with defaults at the system and cluster levels:
no : assertions have no run-time effect.
require : monitor preconditions only, on routine entry.
ensure : preconditions on entry, postconditions on exit.
invariant : same as ensure, plus class invariant on both entry and exit for qualified calls.
all : same as invariant, plus check instructions, loop invariants and loop variants.
An assertion violation, if detected at run time under one of these options other than the first, will cause an exception ( "Exception handling" ). Unless the software has an explicit "retry" plan as explained in the discussion of exceptions, the violation will produce an exception trace and cause termination (or, in EiffelStudio, a return to the environment's browsing and debugging facilities at the point of failure). If present, the label of the violated sub clause will be displayed, to help identify the problem.
The default is require. This is particularly interesting in connection with the Eiffel method's insistence on reuse: with libraries such as EiffelBase, richly equipped with preconditions expressing terms of use, an error in the client software will often lead, for example through an incorrect argument, to violating one of these preconditions. A somewhat paradoxical consequence is that even an application developer who does not apply the method too well (out of carelessness, haste, indifference or ignorance) will still benefit from the presence of contracts in someone else's library code.
During development and testing, assertion monitoring should be turned on at the highest possible level. Combined with static typing and the immediate feedback of compilation techniques such as the Melting Ice Technology, this permits the development process mentioned in the section "Quality and functionality", where errors are exterminated at birth. No one who has not practiced the method in a real project can imagine how many mistakes are found in this way; surprisingly often, a violation will turn out to affect an assertion that was just included for goodness' sake, the developer being convinced that it could never "possibly" fail to be satisfied.
By providing a precise reference (the description of what the software is supposed to do) against which to assess the reality (what the software actually does), Design by Contract profoundly transforms the activities of debugging, testing and quality assurance.
When releasing the final version of a system, it is usually appropriate to turn off assertion monitoring, or bring it down to the require level. The exact policy depends on the circumstances; it is a trade off between efficiency considerations, the potential cost of mistakes, and how much the developers and quality assurance team trust the product. When developing the software, however, you should always assume -- to avoid loosening your guard -- that in the end monitoring will be turned off.
Another application of assertions governs documentation. Environment mechanisms, such as clicking the Form Contract icon in Eiffelstudio, will produce, from a class text, an abstracted version which only includes the information relevant for client authors. Here is the contract form of class ACCOUNT in the latest version given:
note description: "Simple bank accounts" class interface ACCOUNT feature -- Access balance: INTEGER -- Current balance deposit_count: INTEGER -- Number of deposits made since opening feature -- Element change deposit (sum: INTEGER) -- Add `sum' to account. require non_negative: sum >= 0 ensure one_more_deposit: deposit_count = old deposit_count + 1 updated: balance = old balance + sum invariant consistent_balance: balance = all_deposits.total end -- class interface ACCOUNT
The words class interface are used instead of just class to avoid any confusion with actual Eiffel text, since this is documentation, not executable software. (It is in fact possible to generate a compilable variant of the Contract Form in the form of a deferred class, a notion defined later.)
Compared to the full text, the Contract Form of a class (also called its "short form") retains all its interface properties, relevant to client authors:
The following elements, however, are not in the Contract Form: any information about non-exported features; all the routine bodies (do clauses, or the external and once variants seen in "External software" above and "Once routines and shared objects" below); assertion subclauses involving non-exported features; and some keywords not useful in the documentation.
In accordance with the Uniform Access principle (described in "Objects, fields, values, and references" ), the Contract Form does not distinguish between attributes and argument-less queries. In the above example, balance could be one or the other, as it makes no difference to clients, except possibly for performance.
The Contract Form is the fundamental tool for using supplier classes in the Eiffel method. It enables client authors to reuse software elements without having to read their source code. This is a crucial requirement in large-scale industrial developments.
The Contract Form satisfies two key requirements of good software documentation:
The Contract Form is only one of the relevant views. EiffelStudio, for example, generates graphical representations of system structures, to show classes and their relations -- client, inheritance -- according to the conventions of BON (the Business Object Notation). In accordance with the principles of seamlessness and reversibility, EiffelStudio lets you both work on the text, producing the graphics on the fly, or work on the graphics, updating the text on the fly; you can alternate as you wish between these two modes. The resulting process is quite different from more traditional approaches based on separate tools: an analysis and CASE workbench, often based on UML, to deal with an initial "bubble-and-arrow" description; and a separate programming environment, to deal with implementation aspects only. In Eiffel the environment provides consistent, seamless support from beginning to end.
The Contract Form -- or its variant the Flat-Contract Form, which takes account of inheritance ( "Flat and Flat-Contract Forms" ) are the standard form of library documentation, used extensively, for example, in the book Reusable Software (see bibliography). Assertions play a central role in such documentation by expressing the terms of the contract. As demonstrated a contrario by the widely publicized $500-million crash of the Ariane-5 rocket launcher in June of 1996, due to the incorrect reuse of a software module from the Ariane-4 project, reuse without a contract documentation is the path to disaster. Non-reuse would, in fact, be preferable.
Another application of Design by Contract governs the handling of unexpected cases. The vagueness of many discussions of this topic follows from the lack of a precise definition of terms such as "exception". With Design by Contract we are in a position to be specific:
fail, that is to say be unable to meet its contract; for example an arithmetic operation may produce an overflow (a non-representable result).
Note the precise definitions of the two key concepts, failure and exception. Although failure is the more basic one -- since it is defined for atomic, non-routine operations -- the definitions are mutually recursive, since an exception may cause a failure of the recipient routine, and a routine's failure causes an exception in its own caller.
Why state that an exception "may" cause a failure? It is indeed possible to "rescue" a routine from failure in the case of an exception, by equipping it with a clause labeled rescue, as in:
read_next_character (f: FILE) -- Make next character available in last_character. -- If impossible, set failed to True. require readable: file.readable local impossible: BOOLEAN do if impossible then failed := True else last_character := low_level_read_function (f) end rescue impossible := True retry end
This example includes the only two constructs needed for exception handling: rescue and retry. A retry instruction is only permitted in a rescue clause; its effect is to start again the execution of the routine, without repeating the initialization of local entities (such as impossible in the example, which was initialized to False on first entry). Features failed and last_character are assumed to be attributes of the enclosing class.
This example is typical of the use of exceptions: as a last resort, for situations that should not occur. The routine has a precondition, file.readable, which ascertains that the file exists and is accessible for reading characters. So clients should check that everything is fine before calling the routine. Although this check is almost always a guarantee of success, a rare combination of circumstances could cause a change of file status (because a user or some other system is manipulating the file) between the check for readable and the call to low_level_read_function. If we assume this latter function will fail if the file is not readable, we must catch the exception.
A variant would be
local attempts: INTEGER do if attempts < Max_attempts then last_character := low_level_read_function (f) else failed := True end rescue attempts := attempts + 1 retry end
which would try again up to Max_attempts times before giving up.
The above routine, in either variant, never fails: it always fulfills its contract, which states that it should either read a character or set failed to record its inability to do so. In contrast, consider the new variant
local attempts: INTEGER do last_character := low_level_read_function (f) rescue attempts := attempts + 1 if attempts < Max_attempts then retry end end
with no more role for failed. In this case, after Max_attempts unsuccessful attempts, the routine will execute its rescue clause to the end, with no retry (the if having no else clause). This is how a routine fails. It will, as noted, pass on the exception to its caller.
Such a rescue clause should, before terminating, restore the invariant of the class so that the caller and possible subsequent retryattempts from higher up find the objects in a consistent state. As a result, the rule for an absent rescue clause -- the case for the vast majority of routines in most systems -- is that it is equivalent to
rescue
default_rescuewhere procedure default_rescue comes from ANY, where it is defined to do nothing; in a system built for robustness, classes subject to non-explicitly-rescued exceptions should redefine default_rescue (perhaps using a creation procedure, which is bound by the same formal requirement) so that it will always restore the invariant.
Behind Eiffel's exception handling scheme lies the principle -- at first an apparent platitude, but violated by many existing mechanisms -- that a routine should either succeed or fail. This is in turn a consequence of Design by Contract principles: succeeding means being able to fulfill the contract, possibly after one or more retry; failure is the other case, which must always trigger an exception in the caller. Otherwise it would be possible for a routine to miss its contract and yet return to its caller in a seemingly normal state. That is the worst possible way to handle an exception.
Concretely, exceptions may result from the following events:
rescue clause executed to the end with no retry), as just seen.
x.f (...), the fundamental computational mechanism, can only work if x is attached to an object, and will cause an exception otherwise.
It is sometimes useful, when handling exceptions in rescue clauses, to ascertain the exact nature of the exception that got the execution there. For this it is suffices to inherit from the Kernel Library class EXCEPTIONS, which provides queries such as exception, giving the code for the last exception, and symbolic names ( "Constant attributes" ) for all such codes, such as No_more_memory. You can then process different exceptions differently by testing exception against various possibilities. The method strongly suggests, however, that exception handling code should remain simple; a complicated algorithm in a rescue clause is usually a sign that the mechanism is being misused. Class EXCEPTIONS also provides various facilities for fine-tuning the exception facilities, such as a procedure raise that will explicitly trigger a "developer exception" with a code that can then be detected and processed. Exception handling helps produce Eiffel software that is not just correct but robust, by planning for cases that should not normally arise, but might out of Murphy's law, and ensuring they do not affect the software's basic safety and simplicity.
The Design by Contract ideas pervade the Eiffel method. In addition to the applications just mentioned, they have two particularly important consequences:
Inheritance is a powerful and attractive technique. A look at either the practice or literature shows, however, that it is not always well applied. Eiffel has made a particular effort to tame inheritance for the benefit of modelers and software developers. Many of the techniques are original with Eiffel. Paul Dubois has written (comp.lang.python Usenet newsgroup, 23 March 1997): there are two things that [Eiffel] got right that nobody else got right anywhere else: support for design by contract, and multiple inheritance. Everyone should understand these "correct answers" if only to understand how to work around the limitations in other languages.
To make a class inherit from another, simply use an inherit clause:
note ... class D inherit A B ... feature ...
This makes D an heir of A, B and any other class listed. Eiffel supports multiple inheritance: a class may have as many parents as it needs. Later sections ( "Multiple inheritance and renaming" and "Repeated inheritance and selection" ) will explain how to handle possible conflicts between parent features.
Note: This discussion will rely on the terminology introduced in The Static Picture: System Organization: descendants of a class are the class itself, its heirs, the heirs of its heirs and so on. Proper descendants exclude the class itself. The reverse notions are ancestors and proper ancestors.
By default D will simply include all the original features of A, B, ..., to which it may add its own through its feature clauses if any. But the inheritance mechanism is more flexible, allowing D to adapt the inherited features in many ways. Each parent name -- A, B, ... in the example -- can be followed by a Feature Adaptation clause, with subclauses, all optional, introduced by keywords rename, export, undefine, redefine and select, enabling the author of D to make the best use of the inheritance mechanism by tuning the inherited features to the precise needs of D. This makes inheritance a principal tool in the Eiffel process, mentioned earlier, of carefully crafting each individual class, like a machine, for the benefit of its clients. The next sections review the various Feature Adaptation subclauses.
The first form of feature adaptation is the ability to change the implementation of an inherited feature.
Assume a class SAVINGS_ACCOUNT that specializes the notion of account. It is probably appropriate to define it as an heir to class ACCOUNT, to benefit from all the features of ACCOUNT still applicable to savings accounts, and to reflect the conceptual relationship between the two types: every savings account, apart from its own specific properties, also "is" an account. But we may need to produce a different effect for procedure deposit which, besides recording the deposit and updating the balance, may also need, for example, to update the interest.
This example is typical of the form of reuse promoted by inheritance and crucial to effective reusability in software: the case of reuse with adaptation. Traditional forms of reuse are all-or-nothing: either you take a component exactly as it is, or you build your own. Inheritance will get us out of this "reuse or redo" dilemma by allowing us to reuse and redo. The mechanism is feature redefinition:
note description: "Savings accounts" class SAVINGS_ACCOUNT inherit ACCOUNT redefine deposit end feature -- Element change deposit (sum: INTEGER) -- Add sum to account. do ... New implementation (see below) ... end ... Other features ... end -- class SAVINGS_ACCOUNT
Without the redefine subclause, the declaration of deposit would be invalid, yielding two features of the same name, the inherited one and the new one. The subclause makes this valid by specifying that the new declaration will override the old one.
In a redefinition, the original version -- such as the ACCOUNT implementation of deposit in this example -- is called the precursor of the new version. It is common for a redefinition to rely on the precursor's algorithm and add some other actions; the reserved word Precursor helps achieve this goal simply. Permitted only in a routine redefinition, it denotes the parent routine being redefined. So here the body of the new deposit (called "New implementation" above) could be of the form
Precursor (sum) -- Apply ACCOUNT's version of deposit ... Instructions to update the interest ...
In the event that a routine has redefined a particular feature from multiple parent, the Precursor syntax allows the inclusion of a parent qualification:
Precursor {PARENT_X} (args...) -- Apply PARENT_X's version of this feature ... Instructions to update the interest ...
Besides changing the implementation of a routine, a redefinition can turn an argument-less function into an attribute; for example a proper descendant of ACCOUNT could redefine deposits_count, originally a function, as an attribute. The Uniform Access Principle (introduced in The Dynamic Structure: Execution Model ) guarantees that the redefinition makes no change for clients, which will continue to use the feature under the form
acc.deposits_countThe inheritance mechanism is relevant to both roles of classes: module and type. Its application as a mechanism to reuse, adapt and extend features from one class to another, as just seen, covers its role as a module extension mechanism. But it's also a subtyping mechanism. To say that D is an heir of A, or more generally a descendant of A, is to express that instances of D can be viewed as instances of A.
Polymorphic assignment supports this second role. In an assignment x := y, the types of x and y do not have, with inheritance, to be identical; the rule is that the type of y must simply conform to the type of x.
Definition -- Conformance: A class D conforms to a class A if and only if it is a descendant of A (which includes the case in which A and D are the same class); if these classes are generic, conformance of D [U] to C [T] requires in addition that type U conform to type T (through the recursive application of the same rules).
Note: In addition, it will be shown in the discussion of tuples ("Tuple types"), that TUPLE [X] conforms to TUPLE, TUPLE [X, Y] to TUPLE [X] and so on.
So with the inheritance structure that we have seen, the declarations
acc: ACCOUNT sav: SAVINGS_ACCOUNT
make it valid to write the assignment
acc := savwhich will assign to acc a reference attached (if not void) to a direct instance of type SAVINGS_ACCOUNT, not ACCOUNT.
Such an assignment, where the source and target types are different, is said to be polymorphic. An entity such as acc, which as a result of such assignments may become attached at run time to objects of types other than the one declared for it, is itself called a polymorphic entity.
For polymorphism to respect the reliability requirements of Eiffel, it must be controlled by the type system and enable static type checking. We certainly do not want an entity of type ACCOUNT to become attached to an object of type DEPOSIT. Hence the second typing rule:
Rule -- Type Conformance: An assignment x := y, or the use of y as actual argument corresponding to the formal argument x in a routine call, is only valid if the type of y conforms to the the type of x.
The second case listed in the rule is a call such as target.routine(..., y, ...) where the routine declaration is of the form routine (..., x: SOME_TYPE). The relationship between y, the actual argument in the call, and the corresponding formal argument x, is exactly the same as in an assignment x := y: not just the type rule, as expressed by Type Conformance (the type of y must conform to SOME_TYPE), but also the actual run-time effect which, as for assignments, will be either a reference attachment or, for expanded types, a copy.
The ability to accept the assignment x := Void for x of any reference type (see "Basic operations" ) is a consequence of the Type Conformance rule, since Void is of type NONE which by construction ("The global inheritance structure" ) conforms to all types.
Polymorphism also yields a more precise definition of "instance". A direct instance of a type A is an object created from the exact pattern defined by the declaration of A 's base class, with one field for each of the class attributes; you will obtain it through a creation instruction of the form create x ..., for x of type A, or by cloning an existing direct instance. An instance of A is a direct instance of any type conforming to A: A itself, but also any type based on descendant classes. So an instance of SAVINGS_ACCOUNT is also an instance, although not a direct instance, of ACCOUNT.
A consequence of polymorphism is the ability to define polymorphic data structures. With a declaration such as
accounts: LIST [ACCOUNT]
the procedure call accounts.extend ( acc), because it uses a procedure extend which in this case expects an argument of any type conforming to ACCOUNT, will be valid not only if acc is of type ACCOUNT but also if it is of a descendant type such as SAVINGS_ACCOUNT. Successive calls of this kind make it possible to construct a data structure that, at run-time, might contain objects of several types, all conforming to ACCOUNT:
Such polymorphic data structures combine the flexibility and safety of genericity and inheritance. You can make them more or less general by choosing for the actual generic parameter, here ACCOUNT, a type higher or lower in the inheritance hierarchy. Static typing is again essential here, prohibiting for example a mistaken insertion of the form accounts.extend (dep) where dep is of type DEPOSIT, which does not conform to ACCOUNT.
At the higher (most abstract) end of the spectrum, you can produce an unrestrictedly polymorphic data structure general_list: LIST [ANY] which makes the call general_list.extend (x) valid for any x. The price to pay is that retrieving an element from such a structure will yield an object on which the only known applicable operations are the most general ones, valid for all types: assignment, copy, twin, equality comparison and others from ANY. The object test, studied below, will make it possible to apply more specific operations after checking dynamically that a retrieved object is of the appropriate type.
The complement of polymorphism is dynamic binding, the answer to the question "What version of a feature will be applied in a call whose target is polymorphic?".
Consider acc is of type ACCOUNT. Thanks to polymorphism, an object attached to acc may be a direct instance not just of ACCOUNT but also of SAVINGS_ACCOUNT or other descendants. Some of these descendants, indeed SAVINGS_ACCOUNT among them, redefine features such as deposit. Then we have to ask what the effect will be for a call of the form
acc.deposit (some_value)
Dynamic binding is the clearly correct answer: the call will execute the version of deposit from the generating class of the object attached to acc at run time. If acc is attached to a direct instance of ACCOUNT, execution will use the original ACCOUNT version; if acc is attached to a direct instance of SAVINGS_ACCOUNT, the call will execute the version redefined in that class.
This is a clear correctness requirement. A policy of static binding (as available for example in C++ or Delphi, for non-virtual functions) would take the declaration of acc as an ACCOUNT literally. But that declaration is only meant to ensure generality, to enable the use of a single entity acc in many different cases: what counts at execution time is the object that acc represents. Applying the ACCOUNT version to a SAVINGS_ACCOUNT object would be wrong, possibly leading in particular to objects that violate the invariant of their own generating class (since there is no reason a routine of ACCOUNT will preserve the specific invariant of a proper descendant such as SAVINGS_ACCOUNT, which it does not even know about).
In some cases, the choice between static and dynamic binding does not matter: this is the case for example if a call's target is not polymorphic, or if the feature of the call is redefined nowhere in the system. In such cases the use of static binding permits slightly faster calls (since the feature is known at compile time). This application of static binding should, however, be treated as a compiler optimization. The EiffelStudio compiler, under its "finalization" mode, which performs extensive optimization, will detect some of these cases and process them accordingly -- unlike approaches that make developers responsible for specifying what should be static and what dynamic (a tedious and error-prone task, especially delicate because a minute change in the software can make a static call, in a far-away module of a large system, suddenly become dynamic). Eiffel programmers don't need to worry about such aspects; they can rely on the semantics of dynamic binding in all cases, with the knowledge that the compiler will apply static binding when safe and desirable.
Even in cases that require dynamic binding, the design of Eiffel, in particular the typing rules, enable compilers to make the penalty over the static-binding calls of traditional approaches very small and, most importantly, constant-bounded : it does not grow with the depth or complexity of the inheritance structure. The discovery in 1985 of a technique for constant-time dynamic binding calls, even in the presence of multiple and repeated inheritance, was the event that gave the green light to the development of Eiffel.
Dynamic binding is particularly interesting for polymorphic data structures. If you iterate over the list of accounts of various kinds, accounts: LIST [ACCOUNT], illustrated in the last figure, and at each step let acc represent the current list element, you can repeatedly apply
acc.deposit (...)
to have the appropriate variant of the deposit operation triggered for each element.
The benefit of such techniques appears clearly if we compare them with the traditional way to address such needs: using multi-branch discriminating instructions of the form
if "Account is a savings account " then ... elseif "It is a money market account" then ... elseif ...
case ... of ..., switch or inspect instructions. Apart from their heaviness and complexity, such solutions cause many components of a software system to rely on the knowledge of the exact set of variants available for a certain notion, such as bank account. Then any addition, change or removal of variants can cause a ripple of changes throughout the architecture. This is one of the majors obstacles to extendibility and reusability in traditional approaches. In contrast, using the combination of inheritance, redefinition, polymorphism and dynamic binding makes it possible to have a point of single choice -- a unique location in the system which knows the exhaustive list of variants. Every client then manipulates entities of the most general type, ACCOUNT, through dynamically bound calls of the form
acc.some_account_feature (...)
These observations make dynamic binding appear for what it is: not an implementation mechanism, but an architectural technique that plays a key role (along with information hiding, which it extends, and Design by Contract, to which it is linked through the assertion redefinition rules seen below) in providing the modular system architectures of Eiffel, the basis for the method's approach to reusability and extendibility. These properties apply as early as analysis and modeling, and continue to be useful throughout the subsequent steps.
The examples of dynamic binding seen so far assumed that all classes were fully implemented, and dynamically bound features had a version in every relevant class, including the most general ones such as ACCOUNT.
It is also useful to define classes that leave the implementation of some of their features entirely to proper descendants. Such an abstract class is known as deferred; so are its unimplemented features. The reverse of deferred is effective, meaning fully implemented.
LIST is a typical example of deferred class. As it describes the general notion of list, it should not favor any particular implementation; that will be the task of its effective descendants, such as LINKED_LIST (linked implementation), TWO_WAY_LIST (linked both ways) ARRAYED_LIST, (implementation by an array), all effective, and all indeed to be found in EiffelBase.
At the level of the deferred class LIST, some features such as extend (add an item at the end of the list) will have no implementation and hence will be declared as deferred. Here is the corresponding form, illustrating the syntax for both deferred classes and their deferred features:
note description: "[ Sequential finite lists, without a commitment to a representation. ]" deferred class LIST [G] feature -- Access count: INTEGER -- Number of items in list do ... See below; this feature can be effective ... end feature -- Element change extend (x: G) -- Add `x' at end of list. require space_available: not full deferred ensure one_more: count = old count + 1 end ... Other feature declarations and invariants ... end -- class LIST
A deferred feature (considered to be a routine, although it can yield an attribute in a proper descendant) has the single keyword deferred in lieu of the do Instructions clause of an effective routine. A deferred class -- defined as a class that has at least one deferred feature -- must be introduced by deferred class instead of just class.
As the example of extend shows, a deferred feature, although it has no implementation, can be equipped with assertions. They will be binding on implementations in descendants, in a way to be explained below.
Deferred classes do not have to be fully deferred. They may contain some effective features along with their deferred ones. Here, for example, we may express count as a function:
count: INTEGER -- Number of items in list do from start until after loop Result := Result + 1 forth end end
This implementation relies on the loop construct described below ( from introduces the loop initialization) and on a set of deferred features of the class which allow traversal of a list based on moving a fictitious cursor: start to bring the cursor to the first element if any, after to find out whether all relevant elements have been seen, and forth (with precondition not after) to advance the cursor to the next element. Procedure forth itself appears as
forth
-- Advance cursor by one position
require
not_after: not after
deferred
ensure
moved_right: index = old index + 1
endwhere index -- another deferred feature -- is the integer position of the cursor.
Although the above version of feature count is time-consuming -- it implies a whole traversal just for the purpose of determining the number of elements -- it has the advantage of being applicable to all variants, without any commitment to a choice of implementation, as would follow for example if we decided to treat count as an attribute. Proper descendants can always redefine count for more efficiency.
Function count illustrates one of the most important contributions of the method to reusability: the ability to define behavior classes that capture common behaviors (such as count) while leaving the details of the behaviors (such as start, after, forth) open to many variants. As noted earlier, traditional approaches to reusability provide closed reusable components. A component such as LIST, although equipped with directly usable behaviors such as count, is open to many variations, to be provided by proper descendants.
Note: Some O-O languages support only the two extremes: fully effective classes, and fully deferred "interfaces", but not classes with a mix of effective and deferred features. This is an unacceptable limitation, negating the object-oriented method's support for a seamless, continuous spectrum from the most abstract to the most concrete.
A class B inheriting from a deferred class A may provide implementations -- effective declarations -- for the features inherited in deferred form. In this case there is no need for a redefine subclause; the effective versions simply replace the inherited versions. The class is said to effect the corresponding features. If after this process there remain any deferred features, B is still considered deferred, even if it introduces no deferred features of its own, and must be declared as class deferred.
In the example, classes such as LINKED_LIST and ARRAYED_LIST will effect all the deferred features they inherit from LIST -- extend, start etc. -- and hence will be effective.
Except in some applications restricted to pure system modeling -- as discussed next -- the main benefit of deferred classes and features comes from polymorphism and dynamic binding. Because extend has no implementation in class LIST, a call of the form my_list.extend(...) with my_list of type LIST [T] for some T can only be executed if my_list is attached to a direct instance of an effective proper descendant of LIST, such as LINKED_LIST; then it will use the corresponding version of extend. Static binding would not even make sense here.
Even an effective feature of LIST such as count may depend on deferred features (start and so on), so that a call of the form my_list.count can only be executed in the context of an effective descendant.
All this indicates that a deferred class must have no direct instance. (It will have instances, the direct instances of its effective descendants.) If it had any, we could call deferred features on them, leading to execution-time impossibility. The rule that achieves this goal is simple: if the base type of x is a deferred class, no creation instruction of target x, of the form create x..., is permitted.
Deferred classes cover abstract notions with many possible variants. They are widely used in Eiffel where they cover various needs:
These applications make deferred classes a central tool of the Eiffel method's support for seamlessness and reversibility. The last one in particular uses deferred classes and features to model objects from an application domain, without any commitment to implementation, design, or even software (and computers). Deferred classes are the ideal tool here: they express the properties of the domain's abstractions, without any temptation of implementation bias, yet with the precision afforded by type declarations, inheritance structures (to record classifications of the domain concepts), and contracts to express the abstract properties of the objects being described.
Rather than using a separate method and notation for analysis and design, this approach integrates seamlessly with the subsequent phases (assuming the decision is indeed taken to develop a software system): it suffices to refine the deferred classes progressively by introducing effective elements, either by modifying the classes themselves, or by introducing design- and implementation-oriented descendants. In the resulting system, the classes that played an important role for analysis, and are the most meaningful for customers, will remain important; as we have seen ( "Seamlessness and reversibility" ) this direct mapping property is a great help for extendibility.
The following sketch (from the book Object-Oriented Software Construction, 2nd Edition ) illustrates these ideas on the example of scheduling the programs of a TV station. This is pure modeling of an application domain; no computers or software are involved yet. The class describes the notion of program segment.
Note the use of assertions to define semantic properties of the class, its instances and its features. Although often presented as high-level, most object-oriented analysis methods (with the exception of Walden's and Nerson's Business Object Notation) have no support for the expression of such properties, limiting themselves instead to the description of broad structural relationships.
note description: "Individual fragments of a broadcasting schedule" deferred class SEGMENT feature -- Access schedule: SCHEDULE deferred end -- Schedule to which segment belongs index: INTEGER deferred end -- Position of segment in its schedule starting_time, ending_time: INTEGER deferred end -- Beginning and end of scheduled air time next: SEGMENT deferred end -- Segment to be played next, if any sponsor: COMPANY deferred end -- Segment's principal sponsor rating: INTEGER deferred end -- Segment's rating (for children's viewing etc.) Minimum_duration: INTEGER = 30 -- Minimum length of segments, in seconds Maximum_interval: INTEGER = 2 -- Maximum time (seconds) between successive segments feature -- Element change set_sponsor (s: SPONSOR) require not_void: s /= Void deferred ensure sponsor_set: sponsor = s end ... change_next, set_rating omitted ... invariant in_list: (1 <= index) and (index <= schedule.segments.count) in_schedule: schedule.segments.item (index) = Current next_in_list: (next /= Void) implies (schedule.segments.item (index + 1) = next) no_next_if_last: (next = Void) = (index = schedule.segments.count) non_negative_rating: rating >= 0 positive times: (starting_time > 0) and (ending_time > 0) sufficient_duration: ending_time - starting_time >= Minimum_duration decent_interval: (next.starting_time) - ending_time <= Maximum_interval end
Some deferred classes describe a structural property, useful to the description of many other classes. Typical examples are classes of the Kernel Library in EiffelBase:
NUMERIC describes objects on which arithmetic operations +, -, *, / are available, with the properties of a ring (associativity, distributivity, zero elements etc.). Kernel Library classes such as INTEGER and REAL -- but not, for example, STRING -- are descendants of NUMERIC. An application that defines a class MATRIX may also make it a descendant of NUMERIC.
COMPARABLE describes objects on which comparison operations <, <=, >, >= are available, with the properties of a total preorder (transitivity, irreflexivity). Kernel Library classes such as CHARACTER, STRING and INTEGER -- but not our MATRIX example -- are descendants of NUMERIC.
For such classes it is again essential to permit effective features in a deferred class, and to include assertions. For example class COMPARABLE declares infix "<" as deferred, and expresses >, >= and <= effectively in terms of it.
Note: The type like Current will be explained in "Covariance, anchored declarations, and "catcalls"" ; you may understand it, in the following class, as equivalent to COMPARABLE.
note description: "Objects that can be compared according to a total preorder relation" deferred class COMPARABLE feature -- Comparison infix "<" (other: like Current): BOOLEAN -- Is current object less than `other'? require other_exists: other /= Void deferred ensure asymmetric: Result implies not (other < Current) end infix "<=" (other: like Current): BOOLEAN -- Is current object less than or equal to `other'? require other_exists: other /= Void do Result := (Current < other) or is_equal (other) ensure definition: Result = (Current < other) or is_equal (other) end ... Other features: infix ">", min, max, ... invariant irreflexive: not (Current < Current) end -- class COMPARABLE
It is often necessary to define a new class in terms of several existing ones. For example:
The Kernel Library classes INTEGER and REAL must inherit from both NUMERIC and COMPARABLE.
A class TENNIS_PLAYER, in a system for keeping track of player ranking, will inherit from COMPARABLE, as well as from other domain-specific classes.
A class COMPANY_PLANE may inherit from both PLANE and ASSET.
Class ARRAYED_LIST, describing an implementation of lists through arrays, may inherit from both LIST and ARRAY.
In all such cases multiple inheritance provides the answer.
Multiple inheritance can cause name clashes : two parents may include a feature with the same name. This would conflict with the ban on name overloading within a class -- the rule that no two features of a class may have the same name. Eiffel provides a simple way to remove the name clash at the point of inheritance through the rename subclause, as in
note description: "Sequential finite lists implemented as arrays" class ARRAYED_LIST [G] inherit LIST [G] ARRAY [G] rename count as capacity, item as array_item end feature ... end -- class ARRAYED_LIST
Here both LIST and ARRAY have features called count and item. To make the new class valid, we give new names to the features inherited from ARRAY, which will be known within ARRAYED_LIST as capacity and array_item. Of course we could have renamed the LIST versions instead, or renamed along both inheritance branches.
Every feature of a class has a final name : for a feature introduced in the class itself ("immediate" feature) it is the name appearing in the declaration; for an inherited feature that is not renamed, it is the feature's name in the parent; for a renamed feature, it is the name resulting from the renaming. This definition yields a precise statement of the rule against in-class overloading:
Rule -- Final Name: Two different features of a class may not have the same final name.
It is interesting to compare renaming and redefinition. The principal distinction is between features and feature names. Renaming keeps a feature, but changes its name. Redefinition keeps the name, but changes the feature. In some cases, it is of course appropriate to do both.
Renaming is interesting even in the absence of name clashes. A class may inherit from a parent a feature which it finds useful for its purposes, but whose name, appropriate for the context of the parent, is not consistent with the context of the heir. This is the case with ARRAY's feature count in the last example: the feature that defines the number of items in an array -- the total number of available entries -- becomes, for an arrayed list, the maximum number of list items; the truly interesting indication of the number of items is the count of how many items have been inserted in the list, as given by feature count from LIST. But even if we did not have a name clash because of the two inherited count features we should rename ARRAY 's count as capacity to maintain the consistency of the local feature terminology.
The rename subclause appears before all the other feature adaptation subclauses -- redefine already seen, and the remaining ones export, undefine and select -- since an inherited feature that has been renamed sheds its earlier identity once and for all: within the class, and to its own clients and descendants, it will be known solely through the new name. The original name has simply disappeared from the name space. This is essential to the view of classes presented earlier: self-contained, consistent abstractions prepared carefully for the greatest enjoyment of clients and descendants.
A proper understanding of inheritance requires looking at the mechanism in the framework of Design by Contract, where it will appear as a form of subcontracting.
The first rule is that invariants accumulate down an inheritance structure:
Rule -- Invariant Accumulation: The invariants of all the parents of a class apply to the class itself.
The invariant of a class is automatically considered to include -- in the sense of logical "and" -- the invariants of all its parents. This is a consequence of the view of inheritance as an "is" relation: if we may consider every instance of B as an instance of A, then every consistency constraint on instances of A must also apply to instances of B.
Next we consider routine preconditions and postconditions. The rule here will follow from an examination of what contracts mean in the presence of polymorphism and dynamic binding.
Consider a parent A and a proper descendant B (a direct heir on the following figure), which redefines a routine r inherited from A.
As a result of dynamic binding, a call a1.r from a client C may be serviced not by A's version of r but by B 's version if a1, although declared of type A, becomes at run time attached to an instance of B. This shows the combination of inheritance, redefinition, polymorphism and dynamic binding as providing a form of subcontracting; A subcontracts certain calls to B.
The problem is to keep subcontractors honest. Assuming preconditions and postconditions as shown on the last figure, a call in C of the form
if a1.pre then a1.r end
or possibly
a1.q a1.r
where the postcondition of some routine q implies the precondition pre of r, satisfies the terms of the contract and hence is entitled to being handled correctly -- to terminate in a state satisfying a1.post. But if we let the subcontractor B redefine the assertions to arbitrary pre' and post', this is not necessarily the case: pre' could be stronger than pre, enabling B not to process correctly certain calls that are correct from A's perspective; and post' could be weaker than post, enabling B to do less of a job than advertized for r in the Contract Form of A, the only official reference for authors of client classes such as C. (An assertion p is stronger than or equal to an assertion q if p implies q in the sense of boolean implication.)
The rule, then, is that for the redefinition to be correct the new precondition pre' must be weaker than or equal to the original pre, and the new postcondition post' must be stronger than or equal to the original post.
Because it is impossible to check simply that an assertion is weaker or stronger than another, the language rule relies on different forms of the assertion constructs, require else and ensure then, for redeclared routines. They rely on the mathematical property that for any assertions p and q, the following are true:
1) p implies (p or q) 2) (p and q) implies p
require else with a new assertion will perform an or, which can only weaken the original; for a postcondition, ensure then will perform an and, which can only strengthen the original. Hence the rule:
Rule -- Assertion Redeclaration: In the redeclared version of a routine, it is not permitted to use a require or ensure clause. Instead you may: Introduce a new condition with require else, for or-ing with the original precondition. Introduce a new condition with ensure then, for and-ing with the original postcondition. In the absence of such a clause, the original assertions are retained.
The last case -- retaining the original -- is frequent but by no means universal.
The Assertion Redeclaration rule applies to redeclarations. This terms covers not just redefinition but also effecting (the implementation, by a class, of a feature that it inherits deferred). The rules -- not just for assertions but also, as reviewed below, for typing -- are indeed the same in both cases. Without the Assertion Redeclaration rule, assertions on deferred features, such as those on extend, count and forth in "Deferred features and classes" , would be almost useless -- wishful thinking; the rule makes them binding on all effectings in descendants.
From the Assertion Redeclaration rule follows an interesting technique: abstract preconditions. What needs to be weakened for a precondition (or strengthened for a postcondition) is not the assertion's concrete semantics but its abstract specification as seen by the client. A descendant can change the implementation of that specification as it pleases, even to the effect of strengthening the concrete precondition, as long as the abstract form is kept or weakened. The precondition of procedure extend in the deferred class LIST provided an example. We wrote the routine (in "Deferred features and classes" ) as
extend (x: G) -- Add `x' at end of list. require space_available: not full deferred ensure one_more: count = old count + 1 end
The precondition expresses that it is only possible to add an item to a list if the representation is not full. We may well consider -- in line with the Eiffel principle that whenever possible structures should be of unbounded capacity -- that LIST should by default make full always return false:
full: BOOLEAN -- Is representation full? -- (Default: no) do Result := False end
Now a class BOUNDED_LIST that implements bounded-size lists (inheriting, like the earlier ARRAYED_LIST, from both LIST and ARRAY) may redefine full:
full: BOOLEAN -- Is representation full? -- (Answer: if and only if number of items is capacity) do Result := (count = capacity) end
Procedure extend remains applicable as before; any client that used it properly with LIST can rely polymorphically on the FIXED_LIST implementation. The abstract precondition of extend has not changed, even though the concrete implementation of that precondition has in fact been strengthened.
Note that a class such as BOUNDED_LIST, the likes of which indeed appear in EiffelBase, is not a violation of the Eiffel advice to stay away from fixed-size structures. The corresponding structures are bounded, but the bounds are changeable. Although extend requires not full, another feature, called force in all applicable classes, will add an element at the appropriate position by resizing and reallocating the structure if necessary. Even arrays in Eiffel are not fixed-size, and have a procedure force with no precondition, accepting any index position.
The Assertion Redeclaration rule, together with the Invariant Accumulation rule, provides the right methodological perspective for understanding inheritance and the associated mechanisms. Defining a class as inheriting from another is a strong commitment; it means inheriting not only the features but the logical constraints. Redeclaring a routine is bound by a similar committment: to provide a new implementation (or, for an effecting, a first implementation) of a previously defined semantics, as expressed by the original contract. Usually you have a wide margin for choosing your implementation, since the contract only defines a range of possible behaviors (rather than just one behavior), but you must remain within that range. Otherwise you would be perverting the goals of redeclaration, using this mechanism as a sort of late-stage hacking to override bugs in ancestor classes.
It is not an error to inherit two deferred features from different parents under the same name, provided they have the same signature (number and types of arguments and result). In that case a process of feature join takes place: the features are merged into just one -- with their preconditions and postconditions, if any, respectively or-ed and and-ed.
More generally, it is permitted to have any number of deferred features and at most one effective feature that share the same name: the effective version, if present will effect all the others.
All this is not a violation of the Final Name rule (defined in "Multiple inheritance and renaming" ), since the name clashes prohibited by the rule involve two different features having the same final name; here the result is just one feature, resulting from the join of all the inherited versions.
Sometimes we may want to join effective features inherited from different parents, assuming again the features have compatible signatures. One way is to redefine them all into a new version. That is, list each in a redefine clause, then write a redefined version of the feature. In this case, they again become one feature, with no name clash in the sense of the Final Name rule. But in other cases we may simply want one of the inherited implementations to take over the others. The solution is to revert to the preceding case by uneffecting the other features; uneffecting an inherited effective feature makes it deferred (this is the reverse of effecting, which turns an inherited deferred feature into an effective one). The syntax uses the undefine subclause:
class D inherit A rename g as f -- g was effective in A undefine f end B undefine f -- f was effective in B end C -- C also has an effective feature f , which will serve as -- implementation for the result of the join. feature ...
Again what counts, to determine if there is an invalid name clash, is the final name of the features. In this example, two of the joined features were originally called f; the one from A was called g, but in D it is renamed as f, so without the undefinition it would cause an invalid name clash.
Feature joining is the most common application of uneffecting. In some non-joining cases, however, it may be useful to forget the original implementation of a feature and let it start a new life devoid of any burden from the past.
Another Feature Adaptation subclause, export, makes it possible to change the export status of an inherited feature. By default -- covering the behavior desired in the vast majority of practical cases -- an inherited feature keeps its original export status (exported, secret, selectively exported). In some cases, however, this is not appropriate:
A feature may have played a purely implementation-oriented role in the parent, but become interesting to clients of the heir. Its status will change from secret to exported.
In implementation inheritance (for example ARRAYED_LIST inheriting from ARRAY) an exported feature of the parent may not be suitable for direct use by clients of the heir. The change of status in this case is from exported to secret.
You can achieve either of these goals by writing
class D inherit A export {X, Y, ...} feature1, feature2, ... end ...
This gives a new export status to the features listed (under their final names since, as noted, export like all other subclauses comes after rename if present): they become exported to the classes listed. In most cases this list of classes, X, Y, ..., consists of just ANY, to re-export a previously secret feature, or NONE, to hide a previously exported feature. It is also possible, in lieu of the feature list, to use the keyword all to apply the new status to all features inherited from the listed parent. Then there can be more than one class-feature list, as in
class ARRAYED_LIST [G] inherit ARRAY [G] rename count as capacity, item as array_item, put as array_put export {NONE} all {ANY} capacity end ...
where any explicit listing of a feature, such as capacity, takes precedence over the export status specified for all. Here most features of ARRAY are secret in ARRAYED_LIST, because the clients should not permitted to manipulate array entries directly: they will manipulate them indirectly through list features such as extend and item, whose implementation relies on array_item and array_put. But ARRAY's feature count remains useful, under the name capacity, to the clients of ARRAYED_LIST.
Thanks to inheritance, a concise class text may achieve a lot, relying on all the features inherited from direct and indirect ancestors.
This is part of the power of the object-oriented form of reuse, but can create a comprehension and documentation problem when the inheritance structures become deep: how does one understand such a class, either as client author or as maintainer? For clients, the Contract Form, entirely deduced from the class text, does not tell the full story about available features; and maintainers must look to proper ancestors for much of the relevant information.
These observations suggest ways to produce, from a class text, a version that is equivalent feature-wise and assertion-wise, but has no inheritance dependency. This is called the Flat Form of the class. It is a class text that has no inheritance clause and includes all the features of the class, immediate (declared in the class itself) as well as inherited. For the inherited features, the flat form must of course take account of all the feature adaptation mechanisms: renaming (each feature must appear under its final name), redefinition, effecting, uneffecting and export status change. For redeclared features, require else clauses are or-ed with the precursors' preconditions, and ensure then clauses are and-ed with precursors' postconditions. For invariants, all the ancestors' clauses are concatenated. As a result, the flat form yields a view of the class, its features and its assertions that conforms exactly to the view offered to clients and (except for polymorphic uses) heirs.
As with the Contract Form ( "The contract form of a class" ), producing the Flat Form is the responsibility of tools in the development environment. In EiffelStudio, you will just click the "Flat" icon.
The Contract Form of the Flat Form of a class is known as its Flat-Contract Form. It gives the complete interface specification, documenting all exported features and assertions -- immediate or inherited -- and hiding implementation aspects. It is the appropriate documentation for a class.
An inheritance mechanism, following from multiple inheritance, remains to be seen. Through multiple inheritance, a class can be a proper descendant of another through more than one path. This is called repeated inheritance and can be indirect, as in the following figure, or even direct, when a class D lists a class A twice in its inherit clause.
The figure's particular example is in fact often used by introductory presentations of multiple inheritance, which is a pedagogical mistake: simple multiple inheritance examples (such as INTEGER inheriting from NUMERIC and COMPARABLE, or COMPANY_PLANE from ASSET and PLANE) should involve the combination of separate abstractions. Repeated inheritance is an advanced technique; although invaluable, it does not arise in elementary uses and requires a little more care.
In fact there is only one non-trivial issue in repeated inheritance: what does a feature of the repeated ancestor, such as change_address and computer_account, mean for the repeated descendant, here TEACHING_ASSISTANT? (The example features chosen involve a routine and an attribute; the basic rules will be the same.)
There are two possibilities: sharing (the repeatedly inherited feature yields just one feature in the repeated descendant) and duplication (it yields two). Examination of various cases shows quickly that a fixed policy, or one that would apply to all the features of a class, would be inappropriate.
Feature change_address calls for sharing: as a teaching assistant, you may be both teacher and student, but you are just one person, with just one official domicile.
If there are separate accounts for students' course work and for faculty, you may need one of each kind, suggesting that computer_account calls for duplication.
The Eiffel rule enables, once again, the software developer to craft the resulting class so as to tune it to the exact requirements. Not surprisingly, it is based on names, in accordance with the Final Name rule (no in-class overloading):
Rule -- Repeated Inheritance:
A feature inherited multiply under one name will be shared: it is considered to be just one feature in the repeated descendant.
A feature inherited multiply under different names will be replicated, yielding as many variants as names.
So to tune the repeated descendant, feature by feature, for sharing and replication it suffices to use renaming.
Doing nothing will cause sharing, which is indeed the desired policy in most cases (especially those cases of unintended repeated inheritance: making D inherit from A even though it also inherits from B, which you forgot is already a descendant of A).
If you use renaming somewhere along the way, so that the final names are different, you will obtain two separate features. It does not matter where the renaming occurs; all that counts is whether in the common descendant, TEACHING_ASSISTANT in the last figure, the names are the same or different. So you can use renaming at that last stage to cause replication; but if the features have been renamed higher you can also use last-minute renaming to avoid replication, by bringing them back to a single name.
The Repeated Inheritance rule gives the desired flexibility to disambiguate the meaning of repeatedly inherited features. There remains a problem in case of redeclaration and polymorphism. Assume that somewhere along the inheritance paths one or both of two replicated versions of a feature f, such as computer_account in the example, has been redeclared; we need to define the effect of a call a.f ( a.computer_account in the example) if a is of the repeated ancestor type, here UNIVERSITY_PERSON, and has become attached as a result of polymorphism to an instance of the repeated descendant, here TEACHING_ASSISTANT. If one or more of the intermediate ancestors has redefined its version of the feature, the dynamically-bound call has two or moreversions to choose from.
A select clause will resolve the ambiguity, as in
class TEACHING_ASSISTANT inherit TEACHER rename computer_account as faculty_account select faculty_account end STUDENT rename computer_account as student_account end ...
We assume here that that no other renaming has occurred -- TEACHING_ASSISTANT takes care of the renaming to ensure replication -- but that one of the two parents has redefined computer_account, for example TEACHER to express the special privileges of faculty accounts. In such a case the rule is that one (and exactly one) of the two parent clauses in TEACHING_ASSISTANT must select the corresponding version. Note that no problem arises for an entity declared as
ta: TEACHING_ASSISTANTsince the valid calls are of the form ta.faculty_account and ta.student_account, neither of them ambiguous; the call ta.computer_account would be invalid, since after the renamings class TEACHING_ASSISTANT has no feature of that name. The select only applies to a call
up.computer_accountup of type UNIVERSITY_PERSON, dynamically attached to an instance of TEACHING_ASSISTANT; then the select resolves the ambiguity by causing the call to use the version from TEACHER.
So if you traverse a list computer_users: LIST [UNIVERSITY_PERSON] to print some information about the computer account of each list element, the account used for a teaching assistant is the faculty account, not the student account.
You may, if desired, redefine faculty_account in class TEACHING_ASSISTANT, using student_account if necessary, to take into consideration the existence of another account. But in all cases we need a precise disambiguation of what computer_account means for a TEACHING_ASSISTANT object known only through a UNIVERSITY_PERSON entity.
The select is only needed in case of replication. If the Repeated Inheritance rule would imply sharing, as with change_address, and one or both of the shared versions has been redeclared, the Final Name rule makes the class invalid, since it now has two different features with the same name. (This is only a problem if both versions are effective; if one or both are deferred there is no conflict but a mere case of feature joining as explained in "Join and uneffecting" .) The two possible solutions follow from the previous discussions:
If you do want sharing, one of the two versions must take precedence over the other. It suffices to undefine the other, and everything gets back to order. Alternatively, you can redefine both into a new version, which takes precedence over both.
If you want to keep both versions, switch from sharing to replication: rename one or both of the features so that they will have different names; then you must select one of them.
Eiffel's inheritance mechanism has an important application to extending the flexibility of the genericity mechanism. In a class SOME_CONTAINER [G], as noted in "Genericity and Arrays" ), the only operations available on entities of type G, the formal generic parameter, are those applicable to entities of all types. A generic class may, however, need to assume more about the generic parameter, as with a class SORTABLE_ARRAY [G ...] which will have a procedure sort that needs, at some stage, to perform tests of the form
if item (i) < item (j) then ...
item (i) and item (j) are of type G. But this requires the availability of a feature infix "<" in all types that may serve as actual generic parameters corresponding to G. Using the type SORTABLE_ARRAY [INTEGER] should be permitted, because INTEGER has such a feature; but not SORTABLE_ARRAY [COMPLEX] if there is no total order relation on COMPLEX.
To cover such cases, declare the class as
class SORTABLE_ARRAY [G -> COMPARABLE]
-> recalls the arrow of inheritance diagrams; what follows it is a type, known as the generic constraint. Such a declaration means that:
Within the class, you may apply the features of the generic constraint -- here the features of COMPARABLE: infix "<", infix ">" etc. -- to expressions of type G.
A generic derivation is only valid if the chosen actual generic parameter conforms to the constraint. Here you can use SORTABLE_ARRAY [INTEGER] since INTEGER inherits from COMPARABLE, but not SORTABLE_ARRAY [COMPLEX] if COMPLEX is not a descendant of COMPARABLE.
A class can have a mix of constrained and unconstrained generic parameters, as in the EiffelBase class HASH_TABLE [G, H -> HASHABLE] whose first parameter represents the types of objects stored in a hash table, the second representing the types of the keys used to store them, which must be HASHABLE. As these examples suggest, structural property classes such as COMPARABLE, NUMERIC and HASHABLE are the most common choice for generic constraints.
Unconstrained genericity, as in C [G], is defined as equivalent to C [G -> ANY].
Caution: As of version 7.1, the assignment attempt has been marked as obsolete. Use the object test (described below in a variant of this same discussion) instead. The documentation for the assignment attempt will remain during a period of transition, but will be removed at some point in the future.
The Type Conformance rule ( "Polymorphism" ) ensures type safety by requiring all assignments to be from a more specific source to a more general target.
Sometimes you can't be sure of the source object's type. This happens for example when the object comes from the outside -- a file, a database, a network. The persistence storage mechanism( "Deep operations and persistence" ) includes, along with the procedure store seen there, the reverse operation, a function retrieved which yields an object structure retrieved from a file or network, to which it was sent using store. But retrieved as declared in the corresponding class STORABLE of EiffelBase can only return the most general type, ANY; it is not possible to know its exact type until execution time, since the corresponding objects are not under the control of the retrieving system, and might even have been corrupted by some external agent.
In such cases you cannot trust the declared type but must check it against the type of an actual run-time object. Eiffel introduces for this purpose the assignment attempt operation, written
x ?= ywith the following effect (only applicable if x is a writable entity of reference type):
If y is attached, at the time of the instruction's execution to an object whose type conforms to the type of x, perform a normal reference assignment.
Otherwise (if y is void, or attached to a non-conforming object), make x void.
Using this mechanism, a typical object structure retrieval will be of the form
x ?= retrieved if x = Void then "We did not get what we expected" else "Proceed with normal computation, which will typically involve calls of the form x.some_feature " end
As another application, assume we have a LIST [ACCOUNT] and class SAVINGS_ACCOUNT, a descendant of ACCOUNT, has a feature interest_rate which was not in ACCOUNT. We want to find the maximum interest rate for savings accounts in the list. Assignment attempt easily solves the problem:
local s: SAVINGS_ACCOUNT do from account_list.start until account_list.after loop s ?= acc_list.item -- item from LIST yields the element at -- cursor position if s /= Void and then s.interest_rate > Result then -- Using and then (rather than and) guarantees -- that s.interest_rate is not evaluated -- if s = Void is true. Result := s.interest_rate end account_list.forth end end
Note that if there is no savings account at all in the list the assignment attempt will always yield void, so that the result of the function will be 0, the default initialization.
Assignment attempt is useful in the cases cited -- access to external objects beyond the software's own control, and access to specific properties in a polymorphic data structure. The form of the instruction precisely serves these purposes; not being a general type comparison, but only a verification of a specific expected type, it does not carry the risk of encouraging developers to revert to multi-branch instruction structures, for which Eiffel provides the far preferable alternative of polymorphic, dynamically-bound feature calls.
The Type Conformance rule ( "Polymorphism" ) ensures type safety by requiring all assignments to be from a more specific source to a more general target.
Sometimes you can't be sure of the source object's type. This happens for example when the object comes from the outside -- a file, a database, a network. The persistence storage mechanism( "Deep operations and persistence" ) includes, along with the procedure store seen there, the reverse operation, a function retrieved which yields an object structure retrieved from a file or network, to which it was sent using store. But retrieved as declared in the corresponding class STORABLE of EiffelBase can only return the most general type, ANY; it is not possible to know its exact type until execution time, since the corresponding objects are not under the control of the retrieving system, and might even have been corrupted by some external agent.
In such cases you cannot trust the declared type but must check it against the type of an actual run-time object. Eiffel introduces for this purpose the object test operation, using a form of the attached syntax. The complete attached syntax is:
attached {SOME_TYPE} exp as l_exp
and is a boolean-valued expression. So we can use the attached syntax as an object test. A typical object structure retrieval will be of the form
if attached retrieved as l_temp then -- We got what we expected -- Proceed with normal computation, typically involving calls of the form l_temp.some_feature else -- We did not get what we expected" end
The expression `attached retrieved as l_temp' tests retrieved for voidness. If retrieved is not void, that is, retrieved is currently attached to an object, then a fresh local entity l_temp is created, the object is attached to l_temp, and the value of the expression is True. If retrieved is void, then the value of the expression is False.
As another application, assume we have a LIST [ACCOUNT] and class SAVINGS_ACCOUNT, a descendant of ACCOUNT, has a feature interest_rate which was not in ACCOUNT. We want to find the maximum interest rate for savings accounts in the list. Object test easily solves the problem:
do from account_list.start until account_list.after loop if attached {SAVINGS_ACCOUNT} account_list.item as l_s and then l_s.interest_rate > Result then -- Using and then (rather than and) guarantees -- that l_s.interest_rate is not evaluated -- if `attached {SAVINGS_ACCOUNT} account_list.item as l_s' is False. Result := l_s.interest_rate end account_list.forth end end
Note that if there is no savings account at all in the list the object test will never be satisfied, so that the result of the function will be 0, the default initialization.
The object test is useful also in building void-safe software systems.
See Also: More about the attached syntax in the section on Void-safe programming in Eiffel.
The final properties of Eiffel inheritance involve the rules for adapting not only the implementation of inherited features (through redeclaration of either kind, effecting and redefinition, as seen so far) and their contracts (through the Assertion Redeclaration rule), but also their types. More general than type is the notion of a feature's signature, defined by the number of its arguments, their types, the indication of whether it has a result (that is to say, is a function or attribute rather than a procedure) and, if so, the type of the result.
In many cases the signature of a redeclared feature remains the same as the original's. But in some cases you may want to adapt it to the new class. Assume for example that class ACCOUNT has features
owner: HOLDER set_owner (h: HOLDER) -- Make `h' the account owner. require not_void: h /= Void do owner := h end
We introduce an heir BUSINESS_ACCOUNT of ACCOUNT to represent special business accounts, corresponding to class BUSINESS inheriting from HOLDER:
Clearly, we must redefine owner in class BUSINESS_ACCOUNT to yield a result of type BUSINESS; the same signature redefinition must be applied to the argument of set_owner. This case is typical of the general scheme of signature redefinition: in a descendant, you may need to redefine both results and arguments to types conforming to the originals. This is reflected by a language rule:
Rule -- Covariance: In a feature redeclaration, both the result type if the feature is a query (attribute or function) and the type of any argument if it is a routine (procedure or function) must conform to the original type as declared in the precursor version.
The term "covariance" reflects the property that all types -- those of arguments and those of results -- vary together in the same direction as the inheritance structure.
If a feature such as set_owner has to be redefined for more than its signature -- to update its implementation or assertions -- the signature redefinition will be explicit. For example set_owner could do more for business owners than it does for ordinary owners. Then the redefinition will be of the form
set_owner (b: BUSINESS) -- Make b the account owner. do ... New routine body ... end
In other cases, however, the body will be exactly the same as in the precursor. Then explicit redefinition would be tedious, implying much text duplication. The mechanism of anchored redeclaration solves this problem. The original declaration of set_owner in ACCOUNT should be of the form
set_owner (h: like owner) -- Make h the account owner. -- The rest as before: require not_void: h /= Void do owner := h end
A like anchor type, known as an anchored type, may appear in any context in which anchor has a well-defined type; that is, anchor can be an attribute or function of the enclosing class. Then, assuming T is the type of anchor, the type like anchor means the following:
In the class in which it appears, like anchor means the same as T. For example, in set_owner above, the declaration of h has the same effect as if h had been declared of type HOLDER, the type of the anchor owner in class ACCOUNT.
The difference comes in proper descendants: if a type redefinition changes the type of anchor, any entity declared like anchor will be considered to have been redefined too.
This means that anchored declarations are a form of of implicit covariant redeclaration.
In the example, class BUSINESS_ACCOUNT only needs to redefine the type of owner (to BUSINESS). It doesn't have to redefine set_owner except if it needs to change its implementation or assertions.
It is possible to use Current as anchor; the declaration like Current denotes a type based on the current class (with the same generic parameters if any). This is in fact a common case; we saw in "Structural property classes" , that it applies in class COMPARABLE to features such as
is_less alias "<" (other: like Current): BOOLEAN ...
since we only want to compare two comparable elements of compatible types -- but not, for example, integer and strings, even if both types conform to COMPARABLE. (A "balancing rule" makes it possible, however, to mix the various arithmetic types, consistently with mathematical traditions, in arithmetic expressions such as 3 + 45.82 or boolean expressions such as 3 < 45.82.)
Similarly, class ANY declares procedure copy as
copy (other: like Current) ...
with the argument anchored to the current object.
A final, more application-oriented example of anchoring to Current is the feature merge posited in an earlier example (in "The Dynamic Structure: Execution Model" ) with the signature merge (other: ACCOUNT). By using instead merge (other: like Current) we can ensure that in any descendant class -- BUSINESS_ACCOUNT, SAVINGS_ACCOUNT, MINOR_ACCOUNT ... -- an account will only be mergeable with another of a compatible type.
The anchored types shown above specify anchors which are either:
set_owner (h: like owner) or
Current
is_less alias "<" (other: like Current): BOOLEAN.
Declarations can also use qualified anchored types. Consider this possible feature of ACCOUNT:
owner_name: like owner.name
Here the type of owner_name is determined as the type of the feature name as applied to the type of the feature owner of the current class. As you can imagine, for declarations like this to be valid, the feature names name and owner must be the names queries, i. e., the names of attributes or functions.
This notion can be extended to declare the type through multiple levels of remoteness, so patterns like the following can be valid:
f: like a.b.c.d
For example if a class used a list of items of type ACCOUNT, it might include a declaration for that list:
all_accounts: LINKED_LIST [ACCOUNT] -- All my accounts
This class could declare a feature with a qualified anchored type like this:
account_owner_name: like all_accounts.item.owner.name
A qualified anchored type can be qualified also by specifying a type for the qualifier:
owner_name: like {HOLDER}.name
In this case, the type of owner_name is the same as the type of the name feature of type HOLDER.
Anchored declarations serve as another way to make software more concise and more resilient in a changing world. Let's look at one last example of using a qualified anchored type:
a: ARRAY [DATA] ... local idx: like a.lower do from idx := a.lower until idx > a.upper ...
Declaring the local entity idx as the qualified anchored type like a.lower puts this code (well, actually the producer of this code) in the enviable position of never having to worry about what type is used by class ARRAY for its feature lower. So, {ARRAY}.lower could be implemented as INTEGER_32, NATURAL_64, or some other similar type and this code would be fine, even if at some point that type changed.
In our diversion about anchored declarations, we've gotten away from our discussion of covariance. Let's continue that now with a look at a side effect of covariance known as the catcall.
Covariance makes static type checking more delicate; mechanisms of system validity and catcalls address the problem, discussed in detail in the book Object-Oriented Software Construction, 2nd Edition.
The capabilities of polymorphism combined with covariance provide for powerful and flexible modeling. Under certain conditions, though, this flexibility can lead to problems.
In short, you should be careful to avoid polymorphic catcalls. The call part of catcall means feature call. The cat part is an acronym for Changed Availability or Type. What is changing here are features of descendant classes through the adaptation of inheritance. So maybe a descendant class has changed the export status of an inherited feature, so that that feature is not available on instances of the descendant class ... this is the case of changed availability. Or perhaps, through covariant modeling, the type of an argument to a feature in a descendant class has changed ... the case of changed type.
Let's look at an example of changed type, due to covariant modeling. Suppose we have a system which uses the classes depicted on the following diagram:
If in a client class, we declare the following attributes:
my_animal: ANIMAL my_food_stuff: FOOD_STUFF
Also, the class ANIMAL contains the feature:
eat (a_f: FOOD_STUFF) -- Consume `a_f' deferred end
COW as:
eat (a_f: GRASS)
LION as:
eat (a_f: WILDEBEEST_FILET)
So, covariant modeling is used to make the type of the argument for eat appropriate for each of ANIMAL's heirs.
Here's where the problem comes in. It is possible at run-time to attach to my_animal a direct instance of either COW or LION. So, my_animal is a polymorphic attribute. Likewise, it is possible at run-time that my_food_stuff could be attached to a direct instance of either GRASS or WILDEBEEST_FILET.
So, the feature call:
my_animal.eat (my_food_stuff)
eat, we could be causing a COW to engage in the inappropriate practice of eating a WILDEBEEST_FILET.
Because this possibility exists, developers should exercise caution in using polymorphism and covariant modeling.
In version 6.2 of EiffelStudio, a capability was added to detect harmful catcalls at runtime. So, in our example, if we used my_animal.eat (my_food_stuff) only to feed grass to cows and wildebeest filets to lions, then all would be well. But if we attempted to use that same call to feed an inappropriate food to an animal, we would see an exception.
Likewise the compiler in EiffelStudio will produce warnings in cases in which catcalls are possible. Below is an example of the compiler warning issued on the example.
So far, our experience with inheritance is that of "conforming" inheritance ... the most commonly used type of inheritance. Conforming inheritance is what allows a direct instance (in the catcall example above) of COW to be attached at runtime to an entity of type ANIMAL. This can be a powerful modeling capability, but it is this same polymorphism facilitated by conforming inheritance that puts us in the danger of using polymorphic catcalls.
In cases in which polymorphic attachment is not anticipated, the possibility of catcalls can be avoided by using non-conforming inheritance. Non-conforming inheritance is just a more restrictive form of inheritance.
In order to use non-conforming inheritance for a particular parent, we use the marker {NONE} in the appropriate inheritance part of the class:
class MY_HEIR_CLASS inherit MY_CONFORMING_PARENT inherit {NONE} MY_NON_CONFORMING_PARENT ...
Here there are two inherit clauses, one to specify conforming parents, and one to specify non-conforming parents. The clause specifying the conforming inheritance must precede the one specifying the non-conforming inheritance.
Note: According to the Eiffel programming language standard, it is possible to have any number of inherit clauses an any order, however EiffelStudio versions as late as 6.5 allow only one conforming and one non-conforming clause, with the conforming clause preceding the non-conforming one. This restriction will be removed in a future release.
So, in this case, at runtime it is valid for a direct instance of MY_HEIR_CLASS to be attached to an entity of type MY_CONFORMING_PARENT, but not to an entity of type MY_NON_CONFORMING_PARENT. Accordingly, the compiler would reject any code in which an instance of MY_HEIR_CLASS could become attached to an entity of type MY_NON_CONFORMING_PARENT. Because the polymorphic attachment cannot be made, the possibility of a catcall is avoided.
Note: As implemented, non-conforming inheritance mimics a copy/paste operation in which the features of the parent class are copied to the non-conforming heir class with no inheritance linkage maintained. You should keep this fact in mind when using non-conforming inheritance. In particular, once routines are replicated as unrelated features in the heir classes, so they share neither freshness status nor computed value (in the case of functions). Thus, a once function that comes from a non-conforming parent yields a result that is not related to the one returned by the parent's version.
Contents
|
Eiffel has a remarkably small set of instructions. The basic computational instructions have been seen: creation, assignment, procedure call, retry. They are complemented by control structures: conditional, multi-branch, loop, as well as debug and check.
As noted above we have already introduced assignment. But let's take another look at the assignment in the context of the more abstract concept of attachment. Attachment can occur with reference types by assignment such as:
x := yx is the target of the assignment and y is the source. The object associated with y becomes attached to the entity x.
Attachment also occurs in other contexts. For example, when actual arguments are substituted for formal arguments in a call to a routine.
f (w)
f above, the object associated with the actual argument w will be attached to the formal argument for the duration of the execution of f. So, in this case, w can be viewed as the source of the attachment and the formal argument of f is the target.
Other situations in which attachment occurs include creation instructions, attachment of object test local variables, and the attachment of local iteration cursors in the iteration form of the loop construct.
We learned in the section on polymorphism, that the type of the source of an assignment must conform to the type of the assignment's target.
The rule that governs validity of assignments expands upon this and is generalized to apply to all attachments.
Rule -- Assignment: An assignment is valid if and only if the type of its source expression is compatible with the type of its target entity.
The phrase "compatible with" in this rule means that either it "conforms to" or "converts to".
We saw conformance defined in the section on Polymorphism. Convertibility is explained in the section on Other Mechanisms.
A conditional instruction has the form
if ... then ... elseif ... then ... else ... end
elseif ... then ... part (of which there may be more than one) and the else ... part are optional. After if and elseif comes a boolean expression; after then and else come zero or more instructions.
A multi-branch instruction has the form
inspect exp when v1 then inst when v2 then inst2 ... else inst0 end
where the else inst0 part is optional, exp is a character or integer expression, v1, v1, ... are constant values of the same type as exp, all different, and inst0, inst1, inst2, ... are sequences of zero or more instructions.
The effect of such a multi-branch instruction, if the value of exp is one of the vi, is to execute the corresponding insti. If none of the vi matches, the instruction executes inst0, unless there is no else part, in which case it triggers an exception.
Note: Raising an exception is the proper behavior, since the absence of an else indicates that the author asserts that one of the values will match. If you want an instruction that does nothing in this case, rather than cause an exception, use an else part with an empty inst0. In contrast, if c then inst end with no else part does nothing in the absence of an else part, since in this case there is no implied claim that c must hold.
The loop construct provides a flexible framework for iterative computation. Its flexibility lies in how the complete form can be tailored and simplified for certain purposes by including or omitting optional parts.
You'll learn that the loop construct is always used in one of two forms: a base form which allows precise control over details of all loop aspects, and an iteration form which abstracts many of the details and provides a concise notation, ideal for traversing data structures and other objects which support iteration.
We will explore the entire mechanism, but let's approach things a little at a time.
First let's take a look at two examples. These examples accomplish the same goal: they both use a loop to visit and print the content of each node of a linked list of character strings. So, the list in question might be declared like this:
my_list: LINKED_LIST [STRING]
Here's one example:
from my_list.start until my_list.off loop print (my_list.item) my_list.forth end
and the other:
across my_list as ic loop print (ic.item) end
At first observation, it may not appear that both of these examples are using the same language construct. But, indeed, they are simply two different forms of a single language construct, as you will see.
Incidentally, there is no requirement that Loop example 1 occupy multiple lines, and Loop example 2 occupy only one line. Loop example 1 could have been written like this:
from my_list.start until my_list.off loop print (my_list.item) my_list.forth end
In fact, these two examples illustrate the two basic usage forms of the loop construct in Eiffel. The two basic forms can be differentiated by the parts of the construct with which they begin.
The form shown in Loop example 1 begins with an Initialization part ( from my_list.start ), which starts with the keyword from. Let's call this form the base form. So, the type of loop you see in Loop example 1 has been the traditional mechanism for accomplishing iterative computation, including iterating across data structures. However, extensions to Eiffel's loop construct have provided a more concise way of expressing traversing "iterable" structures.
This is the form shown in Loop example 2. It begins with an Iteration part ( across my_list as c ), which starts with the keyword across. We'll call this form the iteration form.
What is happening in Loop example 1? Let's dissect it and see.
First there is the initialization part:
from my_list.start
The first thing to occur in the execution of the base loop is the execution of any instructions in the initialization part (it is permissible for the initialization part to be empty of instructions, but the keyword from must be present to distinguish the base loop form). In our example, the feature start is applied to my_list which will attempt to set the list cursor to the first element in my_list.
The Exit condition part:
until my_list.off
The exit condition part of the loop construct defines the conditions under which the loop body (explained below) should no longer be executed. In our example, the loop will no longer execute if the cursor is "off", that is, there is no current item. So, if the list is empty, the loop body will not execute at all.
The loop body part:
loop print (my_list.item) my_list.forth
loop body part.
The loop body part contains the sequence of instructions to be executed during each iteration. In the example, that includes printing the current list item and then advancing the cursor. At some point, the cursor will pass the last item in the list, causing the exit condition to become true and stop the loop's execution. So, at the risk of stating the obvious, the key to loops that always complete is to ensure that there is something in the loop body that is guaranteed always to cause the exit condition eventually to become true. Loop correctness will discussed in more detail later.
And finally, there's the End part:
endend part.
Now let's examine the iteration form (sometimes called the "across syntax") used in Loop example 2.
The example begins with an iteration part:
across my_list as ic
The iteration form is special in the sense that it is designed to work with objects which are iterable, usually data structures. The iteration form always targets a particular object (usually a data structure) based on a class that inherits, either directly or indirectly from the library class ITERABLE. The iteration part specifies such a target for the iteration, in the case of our example, the target is my_list.
The "as ic" indicates that a local iteration cursor object referenced by the name ic, and available only for the scope of the iteration, will be created to effect the iteration. The element of my_list which is currently referenced by the cursor ic is accessed through ic.item as you see in the loop body:
loop print (ic.item)
loop body part.
Notice that the loop body does not contain the call to the structure's forth feature, as our example in base form did. Neither do you see the call to start nor the check of off in the exit condition. The iteration form abstracts these for you, relieving you of their burden ... while eliminating some opportunities for error.
Notice also that the call "print (ic.item)"" accesses the current item as "ic.item"" versus "my_list.item"" in the base form. This is because in the iteration form, access to the current item is through the cursor variable, "ic" in the case of Loop example 2.
Concerning cursors, both ways of using the loop construct to traverse a structure employ a cursor. In the base form, the cursor is internal to the structure object. In the case of the example, that would be the instance of LINKED_LIST [STRING] called my_list. Applying the feature item to my_list retrieves the list element currently referenced by the cursor. In the iteration version of traversal, the variable ic holds the iteration cursor, external to the list object. So, you apply ic.item to get the current list element. The advantage to the external cursor is that multiple traversals of the structure can occur simultaneously without interfering with one another. This is possible in the base form, but only by saving and restoring the structure's cursor.
Recommended: The iteration form of the loop construct is not appropriate for use in cases in which the target structure may be changed during the traversal. Therefore, if you choose to alter the structure during traversal, you must use the base loop form with explicit cursor manipulation. This is still tricky business, so you should be certain to protect your work with appropriate contracts.
Lastly, of course, the iteration form includes an end part ... at the end.
In Loop example 2, the loop behaves as an instruction. But it is possible to have the iteration loop form behave as a boolean expression. This is helpful in cases in which you might want to ask a question that can be answered by traversing all or part of a structure.
To get this effect, you use the iteration form with one of two alternative body notations, the all body part or the some body part in place of the loop body. When you use either of these notations, the body is a boolean expression. So, the forms for these body parts are:
all boolean_expressionall body part.
some boolean_expressionsome body part.
So, to know if all the strings in my_list have lengths greater than three characters, we could code:
across my_list as ic all ic.item.count > 3 end
To know if at least one string in my_list has a length greater than three characters, we would use the some body part:
across my_list as ic some ic.item.count > 3 end
Of course you can use iteration loops with "all" or "some" bodies in the same way that you would any other boolean expression; in conditionals, for example.
Now that we've seen examples of the two forms of loops and broken down their component parts, we're ready to examine the anatomy of the entire construct in more detail. You may remember from the beginning of this discussion that the flexibility of the loop construct lies in its ability to use or omit its various parts to gain certain effects.
Here are all the possible loop parts, most of which we've seen in examples, in the order in which they must appear when we code them:
| This loop part: | Has this pattern: |
|---|---|
| Iteration part | across expression as identifier
|
| Initialization part | from zero_or_more_instructions
|
| Invariant part | invariant assertion
|
| Exit condition part | until boolean_expression
|
| Body part | loop zero_or_more_instructions or
|
all boolean_expression or
| |
some boolean_expression
| |
| Variant part | variant optional_tag: integer_expression
|
end part | end
|
Apart from seeing examples, it is useful to understand some of the rules of constructing loops from these parts. Here's an informal summary of what you should know about putting together valid loops:
across keyword (iteration form) or the from keyword (base form).
all keyword or the some keyword are only allowed in the absence of an initialization part.
ITERABLE.
There are implications of these rules that are worth understanding. Let's look at some of them.
Consider that all parts must appear in order (1) and that every loop starts with one of two keywords: either across or from (2). Taken together, these imply that it would be invalid for a loop in base form to include an iteration part. However, the opposite is not true. Because the initialization part falls after the iteration part it is possible for a loop in iteration form to contain an initialization part. Imagine for example, that we wanted to compute the sum of the number of characters in all elements of the list of strings in our examples. The initialization part could be used to initialize the sum entity before starting the iteration:
across my_list as ic from sum := 0 loop sum := sum + ic.item.count end
Loops of the base form require an exit condition part (4). This allows the possibility that Iteration loops may contain an exit condition part. Indeed they may, but it is not required. Using an exit condition part in a loop of the iteration can be useful if you want to impose an early exit condition on an iteration. So, extending the previous example, if we wanted to sum the length of elements, but only until we reached an element whose content matched a certain criterion, we could add the exit condition part:
across my_list as ic from sum := 0 until ic.item ~ "Stop now" loop sum := sum + ic.item.count end
For loops of the iteration form, types of iteration targets must be based on classes inheriting from ITERABLE (5). What classes meet this criterion? All the appropriate classes in the EiffelBase library: lists, hash tables, arrays, intervals, etc. Although the details are beyond the scope of this tutorial, you also should recognize the implication that your own classes could be made iterable.
One useful descendant of ITERABLE is the integer interval. The general operator "|..|" provides a concise way of creating the interval between two integers. So, you can use this to loop across a range of integers without a lot of setup. This example:
across 5 |..| 15 as ic loop print (ic.item.out+"%N") end
Also descending from ITERABLE are the iteration cursors themselves. This means that a cursor can be the target of a loop of the iteration form. Consider this example that prints the items in my_list in reverse order:
across my_list.new_cursor.reversed as ic loop print (ic.item) end
new_cursor is applied to my_list. The result is a new iteration cursor for traversing my_list. Then the reversed feature is applied to that result, which itself results in an iteration cursor having the order of the elements reversed. It is this cursor that is used for ic in the traversal.
The only loop parts that we have yet to address are the invariant part and the variant part. These two optional loop parts exist to help guarantee the correctness of loops. The invariant part expresses a loop invariant (not to be confused with class invariants). For the loop to be correct, the instructions in initialization part must ensure that the loop invariant assertion is true, and then every execution of the loop body must preserve the invariant; so the effect of the loop is to yield a state, eventually, in which both the loop invariant and the exit condition are true.
The loop must terminate after a finite number of iterations, of course. This can be guaranteed by including the loop variant part. The variant part provides an integer expression whose value is non-negative after the execution of the instructions in the initialization part. The value of the variant is then decreased by at least one, while remaining non-negative, by any execution of the loop body. Because a non-negative integer cannot be decreased forever, this guarantees that the loop will terminate.
When assertion monitoring is enabled for loop invariants and variants, the integrity of these properties is checked after initialization and after each loop iteration. An exception will be triggered if the loop invariant does not hold, or if the variant either becomes negative or does not decrease.
An occasionally useful instruction is debug (Debug_key, ... ) instructions end where instructions is a sequence of zero or more instructions and the part in parentheses is optional, containing if present one or more strings, called debug keys. The EiffelStudio compiler lets you specify the corresponding debug compilation option: yes, no, or an explicit debug key. The instructions will be executed if and only if the corresponding option is on. The obvious use is for instructions that should be part of the system but executed only in some circumstances, for example to provide extra debugging information.
The final instruction is connected with Design by Contract. The instruction
check Assertion end
Check level or higher. If so it will evaluate all the assertions listed, having no further effect if they are all satisfied; if any one of them does not hold, the instruction will trigger an exception.
This instruction serves to state properties that are expected to be satisfied at some stages of the computation -- other than the specific stages, such as routine entry and exit, already covered by the other assertion mechanisms such as preconditions, postconditions and invariants. A recommended use of check involves calling a routine with a precondition, where the call, for good reason, does not explicitly test for the precondition. Consider a routine of the form
r (a_count: INTEGER) require valid_count: a_count >= minimum_allowable do ... end
This routine will only work if its precondition is satisfied on entry. To guarantee this precondition, the caller may protect it by the corresponding test, as in
if my_count >= a.minimum_allowable then a.r (my_count) end
In effect, this says that if the value of my_count meets r's precondition requirement, then call r, otherwise continue execution. This implies that there is something useful to be done in the case that the call to r could not be executed because the value of my_count did not meet the precondition.
But suppose that due to previous processing, it is reasonably expected that my_count should always have a value that complies with r's precondition. In other words, it would always be expected that the call to r should proceed without failure. In this case it might be a good idea to use a check to document this property,
check my_count_is_large_enough: my_count >= a.minimum_allowable -- Should always be large enough because ... end
In production (finalized) mode, when assertion monitoring is typically turned off, this instruction will have no effect. But it will be precious for a maintainer of the software who is trying to figure out what it does, and in the process to reconstruct the original developer's reasoning. (The maintainer might of course be the same person as the developer, six months later.) And if the rationale is wrong somewhere, turning assertion checking on will immediately uncover the bug.
There is, however, one form of check that continues to be monitored even when assertion monitoring is turned off.
check Assertion then Compound end
Assertion is a list of assertions as above, and Compound is a list of zero or more executable instructions.
This variant is used often when ensuring void-safety. It is used make certain that certain detachable entities are actually attached to objects when expected, and to create a new void-safe scope for accessing the objects. For example:
check attached my_detachable as l_temp then l_temp.do_something end
my_detachable is attached to an object (as is expected), the local entity l_temp will allow controlled access to the object during the scope of the check instruction. If a case occurs in which my_detachable is not attached to an object, then an exception is triggered. As noted above, for this variant of check, assertion monitoring is always in effect, even if it has been turned off for other cases.
So, the form check ... then ... end is somewhat similar to if ... then ... end. The difference is that the if ... then ... end allows the possibility that valid cases might occur in which the boolean expression is not true, and processing continues. The check ... then ... end does not allow such a possibility. The boolean expression is expected always to hold. In fact, if the expression is not true, then like other assertion violations, this is indicative of a bug, and will cause an exception to be raised.
Recommended: An extra indentation of the check part to separate it from the algorithm proper; and inclusion of a comment listing the rationale behind the developer's decision not to check explicitly for the precondition.
Contents
|
The Eiffel's method obsession with extendibility, reusability and maintainability yields, as has been seen, modular and decentralized architectures, where inter-module coupling is limited to the strictly necessary, interfaces are clearly delimited, and all the temptations to introduce obscure dependencies, in particular global variables, have been removed. There is a need, however, to let various components of a system access common objects, without requiring their routines to pass these objects around as arguments (which would only be slightly better than global variables). For example various classes may need to perform output to a common "console window", represented by a shared object.
Eiffel addresses this need through an original mechanism that also takes care of another important issue, poorly addressed by many design and programming approaches: initialization. The idea is simple: if instead of do the implementation of an effective routine starts with the keyword once, it will only be executed the first time the routine is called during a system execution (or, in a multi-threaded environment, the first time in each thread), regardless of what the caller was. Subsequent calls from the same caller or others will have no effect; if the routine is a function, it will always return the result computed by the first call -- object if an expanded type, reference otherwise.
In the case of procedures, this provides a convenient initialization mechanism. A delicate problem in the absence of a once mechanism is how to provide the users of a library with a set of routines which they can call in any order, but which all need, to function properly, the guarantee that some context had been properly set up. Asking the library clients to precede the first call with a call to an initialization procedure setup is not only user-unfriendly but silly: in a well-engineered system we will want to check proper set-up in every one of the routines, and report an error if necessary; but then if we were able to detect improper set-up we might as well shut up and set up ourselves (by calling setup). This is not easy, however, since the object on which we call setup must itself be properly initialized, so we are only pushing the problem further. Making setup a once procedure solves it: we can simply include a call
setup
at the beginning of each affected routine; the first one to come in will perform the needed initializations; subsequent calls will have, as desired, no effect.
Once functions will give us shared objects. A common scheme is
console: WINDOW -- Shared console window once create Result.make (...) end
Whatever client first calls this function will create the appropriate window and return a reference to it. Subsequent calls, from anywhere in the system, will return that same reference. The simplest way to make this function available to a set of classes is to include it in a class SHARED_STRUCTURES which the classes needing a set of related shared objects will simply inherit.
For the classes using it, console, although a function, looks very much as if it were an attribute -- only one referring to a shared object.
The "Hello World" system at the beginning of this discussion used an output instruction of the form io. put_string (some_string). This is another example of the general scheme illustrated by console. Feature io, declared in ANY and hence usable by all classes, is a once function that returns an object of type STANDARD_FILES (another Kernel Library class) providing access to basic input and output features, one of which is procedure put_string. Because basic input and output must all work on the same files, io should clearly be a once function, shared by all classes that need these mechanisms.
Sometimes it is helpful to adjust the way that once routines work, and that is done by applying once keys. For example, in multithreaded mode, it is reasonable most often for once routines to be executed once per thread, versus once per process. Therefore, the default once syntax, as shown in the example above, would behave as once per thread in multithreaded mode.
Sometimes, however, it is useful in multithreaded mode to create an object which can be shared among threads. To do this, once per process is needed. To create effects like this which are outside the default behavior, we can use once "keys". In following example, a once key is used to specify that the once routine is executed only once per process:
shared_object: SOME_TYPE -- An object that can be shared among threads -- without being reinitialized. once ("PROCESS") create Result.make (...) end
Other valid once keys are "THREAD" and "OBJECT". Of course, "THREAD" ensures that the once routine executes only the first time it is called during the execution of a particular process thread. "OBJECT" is used when it is desirable to have a once routine executed on a once per object basis.
| Once key ... | Routine executed the first time it is called ... |
|---|---|
| PROCESS | During process execution |
| THREAD | During each process thread execution |
| OBJECT | By each instance |
THREAD is the default once key if none is specified (which for single threaded systems will have the same effect as PROCESS).
The concept of once keys is open ended, so additional keys may become supported in the future to allow even finer grained control of once routine behavior.
Warning: As of version 6.6, once per object is implemented using 2 or 3 implementation attributes (these are used to store whether the once routine has already called or not, the eventual exception if any, and the result value if any).
The implementation attributes are named starting with an underscore '_', and if you use the class INTERNAL, the implementation attributes will be included in the field_count, and available through the INTERNAL features.
However this might change in the future, and the implementation attributes might be hidden, so you should not rely on them for your applications.
One last technical detail is that for now a once per object is transient (i.e the associated implementation attributes are transient).
It is possible that during the execution that happens when a once routine is called for the first time, an exception may occur. If this happens, then the same exception will be raised on each subsequent call to the once routine.
The syntax shown above is the current standard syntax. However in Eiffel code written for previous versions, you may run across once keys for multithreaded systems which are expressed in a different syntax. Specifically, the older syntax used a feature's note clause to specify a once key, as in the following example.
shared_object: SOME_TYPE -- Obsolete syntax -- An object that can be shared among threads -- without being reinitialized. note once_status: global once create Result.make (...) end
We now examine a few important mechanisms that complement the preceding picture.
Contents
|
Sometimes we want to provide in software text a self-denoting value of a particular type. In Eiffel this is what we call a manifest constant. For example, if we are searching an indexed structure, we might have an integer variable that we would want to initialize to reference the first item in the structure:
my_index := 1
In this case we used a manifest constant, 1, to provide an initial value for my_index. In particular, this is a manifest integer.
Eiffel also supports manifest constants for real (and double) numbers (ex: 3.1415), boolean values (ex: True, False), and characters (ex: 'A', with special characters expressed using a percent sign as in '%N' for new line, '%B' for backspace, '%"' for double quote, and '%U' for null).
Manifest constants are also available for strings, using double quotes as in: "Hello world!". As with character constants, special characters are denoted using the % codes.
You may occasionally have a need for a manifest string that represents a multi-line formatted string. In Eiffel we call this type of manifest string a verbatim string, and there is a special syntax for specifying verbatim strings in Eiffel code. Verbatim strings are either aligned or non-aligned. Aligned verbatim strings will automatically be adjusted so that their leftmost line (the line with text characters closest to the left margin) contains no "white space" to the left of the first text character. For non-aligned verbatim strings, the white space is left untouched. You use a slightly different way of specifying each type of string. For example, this aligned verbatim string:
my_aligned_string: STRING = "[ Thrice hail the still unconquered King of Song! For all adore and love the Master Art That reareth his throne in temple of the heart; And smiteth chords of passion full and strong Till music sweet allures the sorrowing throng! ]"
will print like this:
Thrice hail the still unconquered King of Song!
For all adore and love the Master Art
That reareth his throne in temple of the heart;
And smiteth chords of passion full and strong
Till music sweet allures the sorrowing throng!The same string, declared as a non-aligned verbatim string:
my_non_aligned_string: STRING = "{ Thrice hail the still unconquered King of Song! For all adore and love the Master Art That reareth his throne in temple of the heart; And smiteth chords of passion full and strong Till music sweet allures the sorrowing throng! }"
will print like this:
Thrice hail the still unconquered King of Song!
For all adore and love the Master Art
That reareth his throne in temple of the heart;
And smiteth chords of passion full and strong
Till music sweet allures the sorrowing throng!The difference in declaration is that the aligned verbatim string uses as its "opener" the double-quote plus bracket combination, " "[ ", and the bracket plus double quote, " ]" ", as its "closer". The non-aligned verbatim string uses braces, " { " and " } " instead of the bracket.
The syntax for specifying verbatim strings allows an option for the situation in which the specified string might conflict with the "closer". You can include a simple string between the double quote and the bracket on each end of the verbatim string to guarantee uniqueness. Here's our aligned verbatim string with the simple string " *? " inserted in the opener and closer:
my_aligned_string: STRING = "*?[ Thrice hail the still unconquered King of Song! For all adore and love the Master Art That reareth his throne in temple of the heart; And smiteth chords of passion full and strong Till music sweet allures the sorrowing throng! ]*?"
The attributes studied earlier were variable: each represents a field present in each instance of the class and changeable by its routines.
It is also possible to declare constant attributes, as in
Solar_system_planet_count: INTEGER = 9
These will have the same value for every instance and hence do not need to occupy any space in objects at execution time. (In other approaches similar needs would be addressed by symbolic constants, as in Pascal or Ada, or macros, as in C.)
What comes after the = is a manifest constant. So you can declare a constant attribute for any type for which there is a manifest constant.
One of the conditions for producing truly great reusable software is to recognize that although you should try to get everything right the first time around you won't always succeed. But if "good enough" may be good enough for application software, it's not good enough, in the long term, for reusable software. The aim is to get ever closer to the asymptote of perfection. If you find a better way, you must implement it. The activity of generalization, discussed as part of the lifecycle, doesn't stop at the first release of a reusable library.
This raises the issue of backward compatibility: how to move forward with a better design, without compromising existing applications that used the previous version?
The notion of obsolete class and feature helps address this issue. By declaring a feature as obsolete, using the syntax
enter (i: INTEGER; x: G) obsolete "Use ` put (x, i)' instead " require ... do put (x, i) end
you state that you are now advising against using it, and suggest a replacement through the message that follows the keyword obsolete, a mere string. The obsolete feature is still there, however; using it will cause no other harm than a warning message when someone compiles a system that includes a call to it. Indeed, you don't want to hold a gun to your client authors' forehead ("Upgrade now or die !"); but you do want to let them know that there is a new version and that they should upgrade at their leisure.
Besides routines, you may also mark classes as obsolete.
The example above is a historical one, involving an early change of interface for the EiffelBase library class ARRAY; the change affected both the feature's name, with a new name ensuring better consistency with other classes, and the order of arguments, again for consistency. It shows the recommended style for using obsolete:
It is good discipline not to let obsolete elements linger around for too long. The next major new release, after a suitable grace period, should remove them.
The design flexibility afforded by the obsolete keyword is critical to ensure the harmonious long-term development of ambitious reusable software.
The basic forms of creation instruction, and the one most commonly used, are the two illustrated earlier ( "Creating and initializing objects" ):
create x.make (2000) create x
the first one if the corresponding class has a create clause, the second one if not. In either form you may include a type name in braces, as in
create {SAVINGS_ACCOUNT} x.make (2000)
which is valid only if the type listed, here SAVINGS_ACCOUNT, conforms to the type of x, assumed here to be ACCOUNT. This avoids introducing a local entity, as in
local xs: SAVINGS_ACCOUNT do create xs.make (2000) x := xs ...
and has exactly the same effect. Another variant is the creation expression, which always lists the type, but returns a value instead of being an instruction. It is useful in the following context:
some_routine (create {ACCOUNT}.make (2000))
which you may again view as an abbreviation for a more verbose form that would need a local entity, using a creation instruction:
local x: ACCOUNT do create x.make (2000) some_routine (x) ...
Unlike creation instructions, creation expressions must always list the type explicitly, {ACCOUNT} in the example. They are useful in the case shown: creating an object that only serves as an argument to be passed to a routine. If you need to retain access to the object through an entity, the instruction create x ... is the appropriate construct.
The creation mechanism gets an extra degree of flexibility through the notion of default_create. The simplest form of creation instruction, create x without an explicit creation procedure, is actually an abbreviation for create x.default_create, where default_create is a procedure defined in class ANY to do nothing. By redefining default_create in one of your classes, you can ensure that create x will take care of non-default initialization (and ensure the invariant if needed). When a class has no create clause, it's considered to have one that lists only default_create. If you want to allow create x as well as the use of some explicit creation procedures, simply list default_create along with these procedures in the create clause. To disallow creation altogether, include an empty create clause, although this technique is seldom needed since most non-creatable classes are deferred, and one can't instantiate a deferred class.
One final twist is the mechanism for creating instances of formal generic parameters. For x of type G in a class C [G], it wouldn't be safe to allow create x, since G stands for many possible types, all of which may have their own creation procedures. To allow such creation instructions, we rely on constrained genericity. You may declare a class as
[G -> T create cp end]
to make G constrained by T, as we learned before, and specify that any actual generic parameter must have cp among its creation procedures. Then it's permitted to use create x.cp, with arguments if required by cp, since it is guaranteed to be safe. The mechanism is very general since you may use ANY for T and default_create for cp. The only requirement on cp is that it must be a procedure of T, not necessarily a creation procedure; this permits using the mechanism even if T is deferred, a common occurrence. It's only descendants of T that must make cp a creation procedure, by listing it in the create clause, if they want to serve as actual generic parameters for C.
The Eiffel model for object-oriented computation involves the application of some feature f to some object x, and possibly passing arguments a:
x.f (a)
This type of feature call is known as an object call because it applies the feature to a target object, in this case x. However, under certain circumstances we may apply a feature of a class in a fashion that does not involve a target object. This type of call is a non-object call. In place of the target object, the syntax of the non-object call uses the type on which the feature can be found.
circumference := radius * 2.0 * {MATH_CONST}.Pi
In the sample above, the call to feature {MATH_CONST}.Pi is a non-object call. This case illustrates one of the primary uses of non-object calls: constants. The library class MATH_CONST contains commonly used mathematical constants. Non-object calls make it possible to use the constants in MATH_CONST without having to create an instance of MATH_CONST or inherit from it.
The other primary use is for external features. One example is when we use Microsoft .NET classes from Eiffel code and have to access mechanisms for which there is no direct analog in Eiffel. Microsoft .NET supports so-called "static" methods and enumeration types. To access these, we use non-object calls. In the example below, a non-object call is used to access the enumeration CreateNew from the .NET enumeration type System.IO.FileMode.
create my_file_stream.make ("my_file.txt", {FILE_MODE}.create_new)
The validity of a non-object call is restricted in ways that mirror these primary uses. That is, any feature called in a non-object call must be either a constant attribute or an external feature. See the ISO/ECMA Eiffel standard document for additional details.
It is useful at times to designate that instances of one type can be created through the controlled conversion of instances of some other type. This can be done through a safe Eiffel type conversion mechanism called convertibility.
Convertibility is useful when refactoring, moving from one design to another, or, as you will see in the example, accommodating external technologies over which we have no control.
Definition -- Convertibility: converts to, converts from:
A type U based on a class CU converts to a type T based on a class CT (and T converts from U) if either
CT has a conversion procedure using U as a conversion type, or
CU has a conversion query listing T as a conversion type,
but not both.
Before we get into an example of convertibility, let's list some of its underlying principles:
Let's look at an example with which you may already be familiar.
my_string: STRING_8 -- Native Eiffel string my_system_string: SYSTEM_STRING -- Native Microsoft .Net string … my_string := my_system_string
In the snippet above, we have attributes declared of type STRING_8 and SYSTEM_STRING.
We know that if we have a attribute of type STRING_8 that we can make a direct assignment of a .Net type of string (that is, the .Net type System.String which we see as class SYSTEM_STRING) to our STRING_8 attribute.
We know also that SYSTEM_STRING does not conform to STRING_8, so according to the definition of compatibility, this must happen through conversion.
Therefore SYSTEM_STRING converts to STRING_8. And according to the definition above this means that either:
SYSTEM_STRING has a conversion query listing STRING_8 as a conversion type, or
STRING_8 has a conversion procedure listing SYSTEM_STRING as a conversion type
In this case STRING_8 has a conversion procedure for objects of type SYSTEM_STRING. Conversion procedures are always creation procedures. So they appear in both the create and the convert parts of the class.
class STRING_8 … create make_from_cil … convert make_from_cil ({SYSTEM_STRING}) …
We won't show the implementation of the conversion procedure, but as you can imagine, it initializes its target with the content of its argument.
Because of convertibility, this code:
my_string := my_system_stringis equivalent to:
create my_string.make_from_cil (my_system_string)
So, we've seen how SYSTEM_STRING converts to STRING_8. But, in the context of our example, we could also do this:
my_system_string := my_stringWhich means that STRING_8 converts to SYSTEM_STRING. The convert part of class STRING_8 also has a conversion query listing SYSTEM_STRING as a conversion type:
class STRING_8 … create make_from_cil … convert make_from_cil ({SYSTEM_STRING}) to_cil: {SYSTEM_STRING} …
Because of convertibility, this code:
my_system_string := my_stringis equivalent to:
my_system_string := my_string.to_cil
You should bear in mind that assignments are not the only situation in which conversions take place. Convertibility works for other types of attachments as well. For example, if a routine calls for an argument of type SYSTEM_STRING, and you supply an actual argument of type STRING_8, this constitutes an attachment, and the conversion from STRING to SYSTEM_STRING will occur.
The study of genericity described arrays. Another common kind of container objects bears some resemblance to arrays: sequences, or "tuples", of elements of specified types. The difference is that all elements of an array were of the same type, or a conforming one, whereas for tuples you will specify the types we want for each relevant element. A typical tuple type is of the form
TUPLE [X, Y, Z]
denoting a tuple of at least three elements, such that the type of the first conforms to X, the second to Y, and the third to Z.
You may list any number of types in brackets, including none at all: TUPLE, with no types in brackets, denotes tuples of arbitrary length.
Info: The syntax, with brackets, is intentionally reminiscent of generic classes, but TUPLE is a reserved word, not the name of a class; making it a class would not work since a generic class has a fixed number of generic parameters. You may indeed use TUPLE to obtain the effect of a generic class with a variable number of parameters.
To write the tuples themselves -- the sequences of elements, instances of a tuple type -- you will also use square brackets; for example
[x1, y1, z1]
with x1 of type X and so on is a tuple of type TUPLE [X, Y, Z].
The definition of tuple types states that TUPLE [X1 ... Xn] denotes sequences of at least n elements, of which the first n have types respectively conforming to X1, ..., Xn. Such a sequence may have more than n elements.
Features available on tuple types include count: INTEGER, yielding the number of elements in a tuple, item (i: INTEGER): ANY which returns the i-th element, and put which replaces an element.
Tuples are appropriate when these are the only operations you need, that is to say, you are using sequences with no further structure or properties. Tuples give you "anonymous classes" with predefined features count, item and put. A typical example is a general-purpose output procedure that takes an arbitrary sequence of values, of arbitrary types, and prints them. It may simply take an argument of type TUPLE, so that clients can call it under the form
write ([your_integer, your_real, your_account])
As soon as you need a type with more specific features, you should define a class.
Our last mechanism, agents, adds one final level of expressive power to the framework describe so far. Agents apply object-oriented concepts to the modeling of operations.
Contents
|
Operations are not objects; in fact, object technology starts from the decision to separate these two aspects, and to choose object types, rather than the operations, as the basis for modular organization of a system, attaching each operation to the resulting modules -- the classes.
In a number of applications, however, we may need objects that represent operations, so that we can include them in object structures that some other piece of the software will later traverse to uncover the operations and, usually, execute them. Such "operation wrapper" objects, called agents, are useful in a number of application areas such as:
Operations in Eiffel are expressed as routines, and indeed every agent will have an associated routine. Remember, however, that the fundamental distinction between objects and operations remains: an agent is an object, and it is not a routine; it represents a routine. As further evidence that this is a proper data abstraction, note that the procedure call, available on all agents to call the associated routine, is only one of the features of agents. Other features may denote properties such as the class to which the routine belongs, its precondition and postcondition, the result of the last call for a function, the number of arguments.
In the simplest form, also one of the most common, you obtain an agent just by writing
agent rwhere r is the name of a routine of the enclosing class. This is an expression, which you may assign to a writable entity, or pass as argument to a routine. Here for example is how you will specify event handling in the style of the EiffelVision 2 GUI library:
your_icon.click_actions.extend (agent your_routine)
This adds to the end of your_icon.click_actions -- the list of agents associated with the "click" event for your_icon, denoting an icon in the application's user interface -- an agent representing your_routine. Then when a user clicks on the associated icon at execution, the EiffelVision 2 mechanisms will call the procedure call on every agent of the list, which for this agent will execute your_routine. This is a simple way to associate elements of your application, more precisely its "business model" (the processing that you have defined, directly connected to the application's business domain), with elements of its GUI.
Similarly although in a completely different area, you may request the integration of a function your_function over the interval 0..1 through a call such as
your_integrator.integral (agent your_function, 0, 1)
In the third example area cited above, you may call an iterator of EiffelBase through
your_list.do_all (agent your_proc)
with your_list of a type such as LIST [YOUR_TYPE]. This will apply your_proc to every element of the list in turn.
The agent mechanism is type-checked like the rest of Eiffel; so the last example is valid if and only if your_proc is a procedure with one argument of type YOUR_TYPE.
An agent agent r built from a procedure r is of type PROCEDURE [T, ARGS] where T represents the class to which r belongs and ARGS the type of its arguments. If r is a function of result type RES, the type is FUNCTION [T, ARGS, RES]. Classes PROCEDURE and FUNCTION are from the Kernel Library of EiffelBase, both inheriting from ROUTINE [T, ARGS].
Among the features of ROUTINE and its descendants the most important are call, already noted, which calls the associated routine, and item, appearing only in FUNCTION and yielding the result of the associated function, which it obtains by calling call.
As an example of using these mechanisms, here is how the function integral could look like in our INTEGRATOR example class. The details of the integration algorithm (straight forward, and making no claims to numerical sophistication) do not matter, but you see the place were we evaluate the mathematical function associated with f, by calling item on f:
integral (f: FUNCTION [ANY, TUPLE [REAL], REAL]; low, high: REAL): REAL -- Integral of `f' over the interval [`low', `high'] require meaningful_interval: low <= high local x: REAL do from x := low invariant x >= low x <= high + step -- Result approximates the integral over -- the interval [low, low.max (x - step)] until x > high loop Result := Result + step * f.item ([x]) -- Here item is applied to f x := x + step end end
Function integral takes three arguments: the agent f representing the function to be integrated, and the two interval bounds. When we need to evaluate that function for the value x, in the line
Result := Result + step * f.item ([x])
we don't directly pass x to item; instead, we pass a one-element tuple [x], using the syntax for manifest tuples introduced in "Tuple types" . You will always use tuples for the argument to call and item, because these features must be applicable to any routine, and so cannot rely on a fixed number of arguments. Instead they take a single tuple intended to contain all the arguments. This property is reflected in the type of the second actual generic parameter to f, corresponding to ARGS (the formal generic parameter of FUNCTION): here it's TUPLE [REAL] to require an argument such as [x], where x is of type REAL.
Similarly, consider the agent that the call seen above:
your_icon.click_actions.extend (agent your_routine)
added to an EiffelVision list. When the EiffelVision mechanism detects a mouse click event, it will apply to each element item of the list of agents, your_icon.click_actions, an instruction such as
item.call ([x, y])
where x and y are the coordinates of the mouse clicking position. If item denotes the list element agent your_routine, inserted by the above call to extend, the effect will be the same as that of calling
your_routine (x, y)
assuming that your_routine indeed takes arguments of the appropriate type, here INTEGER representing a coordinate in pixels. (Otherwise type checking would have rejected the call to extend.)
In the examples so far, execution of the agent's associated routine, through item or call, passed exactly the arguments that a direct call to the routine would expect. You can have more flexibility. In particular, you may build an agent from a routine with more arguments than expected in the final call, and you may set the values of some arguments at the time you define the agent.
Assume for example that a cartographical application lets a user record the location of a city by clicking on the corresponding position on the map. The application may do this through a procedure
Then you can associate it with the GUI through a call such as
map.click_actions.extend (agent record_city (name, population, ?, ?))
assuming that the information on the name and the population has already been determined. What the agent denotes is the same as agent your_routine as given before, where your_routine would be a fictitious two-argument routine obtained from record_city -- a four-argument routine -- by setting the first two arguments once and for all to the values given, name and population.
In the agent agent record_city (name, population, ?, ?), we say that these first two arguments, with their set values, are closed; the last two are open. The question mark syntax introduced by this example may only appear in agent expressions; it denotes open arguments. This means, by the way, that you may view the basic form used in the preceding examples, agent your_routine, as an abbreviation -- assuming your_routine has two arguments -- for agent your_routine (?, ?). It is indeed permitted, to define an agent with all arguments open, to omit the argument list altogether; no ambiguity may result.
For type checking, agent record_city (name, population, ?, ?) and agent your_routine (?, ?) are acceptable in exactly the same situations, since both represent routines with two arguments. The type of both is
where the tuple type specifies the open operands.
A completely closed agent, such as agent your_routine (25, 32) or agent record_city (name, population, 25, 32), has the type TUPLE, with no parameters; you will call it with call ([ ]), using an empty tuple as argument.
The freedom to start from a routine with an arbitrary number of arguments, and choose which ones you want to close and which ones to leave open, provides a good part of the attraction of the agent mechanism. It means in particular that in GUI applications you can limit to the strict minimum the "glue" code (sometimes called the controller in the so-called MVC, Model-View Controller, scheme of GUI design) between the user interface and "business model" parts of a system. A routine such as record_city is a typical example of an element of the business model, uninfluenced -- as it should be -- by considerations of user interface design. Yet by passing it in the form of an agent with partially open and partially closed arguments, you may be able to use it directly in the GUI, as shown above, without any "controller" code.
As another example of the mechanism's versatility, we saw above an integral function that could integrate a function of one variable over an interval, as in
your_integrator.integral (agent your_function (0, 1))
Now assume that function3 takes three arguments. To integrate function3 with two arguments fixed, you don't need a new integral function; just use the same integral as before, judiciously selecting what to close and what to leave open:
your_integrator.integral (agent function3 (3.5, ?, 6.0), 0, 1)
All the agent examples seen so far were based on routines of the enclosing class. This is not required. Feature calls, as you remember, were either unqualified, as in f (x, y), or qualified, as in a.g (x, y). Agents, too, have a qualified variant as in
agent a.g
which is closed on its target a and open on the arguments. Variants such as agent a.g (x, y), all closed, and agent a.g (?, y), open on one argument, are all valid.
You may also want to make the target open. The question mark syntax could not work here, since it wouldn't tell us the class to which feature g belongs, known in the preceding examples from the type of a. As in creation expressions, we must list the type explicitly; the convention is the same: write the types in braces, as in
agent {SOME_TYPE}.g agent {SOME_TYPE}.g (?, ?) agent {SOME_TYPE}.g (?, y)
The first two of these examples are open on the target and both operands; they mean the same. The third is closed on one argument, open on the other and on the target.
These possibilities give even more flexibility to the mechanism because they mean that an operation that needs agents with certain arguments open doesn't care whether they come from an argument or an operand of the original routine. This is particularly useful for iterators and means that if you have two lists
you may write both
your_account_list.do_all (agent {ACCOUNT}.deposit_one_grand) your_integer_list.do_all (agent add_to_total)
even though the two procedures used in the agents have quite different forms. We are assuming here that the first one, a feature of class ACCOUNT, is something like
deposit_one_grand
-- Deposit one thousand into `Current'.
do
deposit (1000)
endThe procedure deposit_one_grand takes no arguments. In the do_all example above, its target is open. The target will be, in turn, each instance of ACCOUNT in your_account_list.
In contrast, the other routine, assumed to be a feature of the calling class, does take an argument x:
add_to_total (x: INTEGER) -- Add `x' to the value of `total'. do total := total + x end
Here, total is assumed to be an integer attribute of the enclosing class. In the do_all example, each instance of your_integer_list will fill the argument x left open in add_to_total.
Without the versatility of playing with open and closed arguments for both the original arguments and target, you would have to write separate iteration mechanisms for these two cases. Here you can use a single iteration routine of LIST and similar classes of EiffelBase, do_all, for both purposes:
In the agent discussion above, it has been assumed that there already exists some routine that we wish to represent with an agent. However, sometimes the only usage of such a routine could be as an agent ... that is, the routine does not make sense as a feature of the class in question. In these cases, we can use inline agents. With an inline agent we write the routine within the agent declaration.
If we consider the use of agents instead of class features in the two do_all examples in the previous section, the agents would be coded as follows:
your_account_list.do_all (agent (a: ACCOUNT) do a.deposit (1000) end)
and
your_integer_list.do_all (agent (i: INTEGER) do total := total + i end)
The syntax of the inline agent corresponds to the syntax of a routine. Immediately following the agent keyword are the formal arguments and in the case of functions the type for Result. Inline agents can have local entities, preconditions, and postconditions, just like any routine.
Inline agents do not have access to the local entities of the routine in which they are coded. So, if it is necessary to use the routine's local variables, they must be passed as arguments to the inline agent.
Here's an example of an inline agent which is a function. It is used in the context of a check to see if every element of your_integer_list is positive:
Inline agents are interesting also as an implementation of the notion of closures in computer science.
Agents provide a welcome complement to the other mechanisms of Eiffel. They do not conflict with them but, when appropriate -- as in the examples sketched in this section -- provide clear and expressive programming schemes, superior to the alternatives.
See Also: Event Programming with Agents
Eiffel software texts are free-format: distribution into lines is not semantically significant, and any number of successive space and line-return characters is equivalent to just one space. The style rules suggest indenting software texts as illustrated by the examples in this chapter.
Successive declarations or instructions may be separated by semicolons. Eiffel's syntax has been so designed, however, that (except in rare cases) the semicolon is optional. Omitting semicolons for elements appearing on separate lines lightens text and is the recommended practice since semicolons, as used by most programming languages, just obscure the text by distracting attention from the actual contents. Do use semicolons if you occasionally include successive elements on a single line.
56 names -- all unabbreviated single English words, except for elseif which is made of two words -- are reserved, meaning that you cannot use them to declare new entities. Here is the list:
agent
| alias
| all
| and
| as
| assign
| check
|
class
| convert
| create
| Current
| debug
| deferred
| do
|
else
| elseif
| end
| ensure
| expanded
| export
| external
|
False
| feature
| from
| frozen
| if
| implies
| indexing
|
infix
| inherit
| inspect
| invariant
| is
| like
| local
|
loop
| not
| obsolete
| old
| once
| or
| prefix
|
Precursor
| pure
| redefine
| reference
| rename
| require
| rescue
|
Result
| retry
| separate
| then
| True
| TUPLE
| undefine
|
Since this tutorial has covered all the essential mechanisms, you may ignore the keywords not encountered; they are reserved for future use.
Most of the reserved words are keywords, serving only as syntactic markers, and written in boldface in typeset texts such as the present one: class, feature, inherit. The others, such as Current, directly carry a semantic denotation; they start with an upper-case letter and are typeset in boldface.
These conventions about letter case are only style rules. Eiffel is case-insensitive, since it is foolish to assume that two identifiers denote two different things just on the basis of a letter written in lower or upper case. The obvious exception is manifest character constants (appearing in single quotes, such as 'A') and manifest character strings (appearing in double quotes, such as "UPPER and lower").
The style rules, however, are precise, and any serious Eiffel project will enforce them; the tools of EiffelStudio also observe them in the texts they output (although they will not mess up with your source text unless you ask them to reformat it). Here are the conventions, illustrated by the examples of this tutorial:
ACCOUNT.
balance and class.
Avogadro and Result.
In typeset documents including Eiffel texts, the standard for font styles is also precise. You should use bold face for keywords and italics for all other Eiffel elements. Comments, however, are typeset in roman. This lets a software element, such as an identifier, stand out clearly in what is otherwise a comment text expressed in English or another human language, as in the earlier example
-- Add `sum' to account.which makes clear that sum is a software element, not the English word.
There is also an Eiffel style to the choice of identifiers. For features, stay away from abbreviations and use full words. In multi-word identifiers, separate the constituents by underscores, as in LINKED_LIST and set_owner. The competing style of no separation but mid-identifier upper-case, as in linkedList or setOwner, is less readable and not in line with standard Eiffel practices.
Features of reusable classes should use consistent names. A set of standard names -- put for the basic command to add or replace an element, count for the query that returns the number of element in a structure, item to access an element -- is part of the style rules, and used systematically in EiffelBase. Use them in your classes too.
For local entities and formal arguments of routines, it is all right to use abbreviated names, since these identifiers only have a local scope, and choosing a loud name would give them too much pretense, leading to potential conflicts with features.
The complete set of style rules applied by ISE is available on the web in both HTML and PDF forms. These rules are an integral part of the Eiffel method; in quality software, there is no such thing as a detail. Applying them systematically promotes consistency between projects in the Eiffel world, enhances reusability, and facilitates everyone's work.
Beyond this introduction, you will find the following books essential to a mastery of the method and language:
Title: An Eiffel Tutorial, Eiffel Software Technical Report TR-EI-66/TU.
Publication history
First published July 2001. Corresponds to release 5.0 of the EiffelStudio environment.
Author
Software credits
See acknowledgments in book Eiffel: The Language.
Cover design
Rich Ayling.
Copyright notice and proprietary information
Copyright Interactive Software Engineering Inc. (Eiffel Software), 2001. May not be reproduced in any form (including electronic storage) without the written permission of Eiffel Software . "Eiffel Power" and the Eiffel Power logo are trademarks of Eiffel Software .
All uses of the product documented here are subject to the terms and conditions of the Eiffel Software Eiffel user license. Any other use or duplication is a violation of the applicable laws on copyright, trade secrets and intellectual property.
Special duplication permission for educational institutions
Degree-granting educational institutions using Eiffel Software Eiffel for teaching purposes as part of the Eiffel University Partnership Program may be permitted under certain conditions to copy specific parts of this book. Contact Eiffel Software for details.
When you develop software in Eiffel, you can be assured (at compile time) that your system will not attempt (at run time) to apply a feature to a void reference. That is, Eiffel prevents situations in which systems fail at run time with the error: "Feature call on void target".
Throughout the history of Eiffel, a small number of important new capabilities, agents for example, have been added. Always these innovations have added significantly to the power of Eiffel, and have been carefully designed to cause a minimum of impact on existing software. Void-safe Eiffel is such an innovation.
However, it is not quite right to refer to it as "void-safe Eiffel". In fact, it is Eiffel ... and it is void-safe, just as it is statically typed.
Still, the reality is that Eiffel did not always provide void-safety, and that has the potential to cause confusion among both new and seasoned Eiffel developers. New developers will find for awhile that parts of the Eiffel documentation were written prior to the advent of void-safety, and may not have been updated yet. Experienced Eiffel programmers will find that software that compiled in versions of Eiffel before it became void-safe, may not compile anymore with void-safe capabilities enabled.
The result is that we must consider certain questions:
To find some answers, continue reading this chapter.
Contents
|
The primary focus of Eiffel is on software quality. Void-safety, like static typing, is another facility for improving software quality. Void-safe software is protected from run time errors caused by calls to void references, and therefore will be more reliable than software in which calls to void targets can occur. The analogy to static typing is a useful one. In fact, void-safe capability could be seen as an extension to the type system, or a step beyond static typing, because the mechanism for ensuring void-safety is integrated into the type system.
You know that static typing eliminates a whole class of software failures. This is done by making an assurance at compile time about a feature call of the form:
x.f (a)
x has a feature f and that any arguments, represented here by a, number the same as the formal arguments of f, and are compatible with the types of those formal arguments.
In statically typed languages like Eiffel, the compiler guarantees that you cannot, at run time, have a situation in which feature f is not applicable to the object attached to x. If you've ever been a Smalltalk programmer, you are certainly familiar with this most common of errors that manifests itself as "Message not understood." It happens because Smalltalk is not statically typed.
Static typing will ensure that there is some feature f that can be applied at run time to x in the example above. But it does not assure us that, in the case in which x is a reference, that there will always be an object attached to x at any time x.f (a) is executed.
This problem is not unique to Eiffel. Other environments that allow or mandate reference semantics also allow the possibility of void-unsafe run time errors. If you've worked in Java or .NET you may have seen the NullReferenceException. Sometimes you might have experienced this rather poetic sounding message: "Object reference not set to an instance of an object". In Eiffel you would see "Feature call on void target". All these are the hallmarks of run time errors resulting from void-unsafe software.
Note: If you need a review of difference between reference types and expanded types in Eiffel, see the chapter of the Eiffel Tutorial dedicated to the Eiffel execution model.
Of course this is not an issue with instances of expanded types, because these instances are indeed "expanded" within their parent objects. But we could not imagine a world with expanded types only. References are important for performance reasons and for modeling purposes. For example, consider that a car has an engine and a manufacturer. When we model cars in software, it might be appropriate for engines to be expanded types, as each car has one engine. But certainly the same is not true for manufacturer. Many cars can share, through a reference, a single manufacturer.
So, references are necessary, but we want them to be trouble free.
Void-safe software, then, is software in which the compiler can give assurance, through a static analysis of the code, that at run time whenever a feature is applied to a reference, that the reference in question will have an object attached. This means that the feature call
x.f (a)
x will be attached to an object when the call executes.
Info: This validity rule is called the Target rule, validity code VUTA, and is the primary rule for void-safety. In the following discussion, you will see that other validity rules are involved, too. You can see the formal definition of all validity rules in the ISO/ECMA standard document available online.
Once we have committed ourselves to this validity rule, we must have a strategy for complying with the rule.
Here are the tools of void-safe trade. They will each be addressed in more detail throughout the documentation that follows. As you look at these elements it helps to try to think about things from the compiler's viewpoint ... after all, it is the compiler that we expect to give us the guarantee that our code is indeed void-safe.
First let's look at a couple of approaches that won't work.
It might occur to us that we could enforce compliance with the target rule by simply eliminating the concept of void references. But this would not be practical. Void is a valuable abstraction that is useful in many situations, such as providing void links in structures. So, we must keep void ... but we want to keep it under control.
Another thought might be that we could just have the compiler do all the work for us. But would be impossibly time consuming for the compiler to investigate every conceivable execution path available to a system to make certain that every possible feature call was made on an attached reference.
So, all of this boils down to the fact that we have to take some actions that help the compiler along. That's what the following are about.
We know that in the context of certain code patterns, it is clear that it would be impossible for a reference to be void. These patterns are identified and we call them CAPs, short for Certified Attachment Patterns. Here is a very straightforward example expressed in a syntax that should be familiar to all Eiffel developers:
if x /= Void then -- ... Any other instructions here that do not assign to x x.f (a) end
x is not void. Then as long as no assignments to x are made in the interim, a feature f can be applied to x with the certainty that x will be attached at the time ... and importantly, this can be determined at compile time. So, we say that this code pattern is a CAP for x.
It is important to understand that in this example (and with other CAPs), x is allowed to be a local variable or formal argument only. That is, x may not be an attribute or general expression (with one exception which we will see below). Direct access to class attribute references cannot be allowed via a CAP due to the fact that they could be set to void by a routine call in some execution path invoked by the intervening instructions or possibly even different process thread. In a later section, we well see that this is not quite such a limitations as it may appear at this point.
Note: You will find more useful information about CAPs in More about CAPs. Learn how certain code patterns are determined to be CAPs in What makes a Certified Attachment Pattern.
For the purposes of void-safety, the attached syntax does double duty for us. It allows us to make certain that a reference is attached, and it provides us a safe way to access objects that are attached to class attributes.
We noted earlier that this code
if x /= Void then -- ... Any other instructions here that do not assign to x x.f (a) end
x, but only if x is a local variable or a formal argument.
By using the attached syntax, we can perform an object test on a variable. That is, the attached syntax is a BOOLEAN expression which provides an answer to the question "Is x attached to an object?" At the same time, if indeed x is attached to an object, the attached syntax will deliver to us a fresh local variable, also attached to x's object, on which we can make feature calls.
if attached x as l_x then l_x.f (a) end
x is tested to make certain that it is attached. If so, the new local l_x becomes attached to the same object as x. And so the object can be used safely even if x is a class attribute. So, the attached syntax, is really another CAP, because it provides a clearly verifiable place for the application of features to targets that are guaranteed not to be void.
Note: The attached syntax has other syntax variations as well as other uses. These will be discussed later.
One way to make sure we comply with the target rule would be always use a CAP or the attached syntax every time we want to apply a feature to a referenced object. That might work, but it falls among the impractical approaches to the problem ... it would break a very high percentage of existing Eiffel code, not to mention cluttering things up quite a bit.
Rather than trying to protect every feature call, Eiffel allows us to declare any variable as being of an attached type. This is an important extension to the Eiffel type system.
In Eiffel prior to the introduction of void-safe facilities, any reference variable could be set to Void. So, all variables were considered '"detachable"'.
The current standard Eiffel supports a mixture of attached and detachable types. When a variable is declared of an attached type, as in the following example, then the compiler will prevent it from being set to Void or set to anything that can be set to Void.
my_attached_string: attached STRING
It is easy to imagine that the more declarations are of attached types, the easier it will be to guarantee that a call to a void target cannot take place at run time. In fact, if every declaration was guaranteed to be of an attached type, then that would be all that was needed to satisfy the Target rule.
However, it wouldn't be workable to have only attached types, because sometimes it's important to allow references to have a value of Void.
When it is necessary to allow Void as a value, a declaration can use the detachable mark as in the following.
my_detachable_string: detachable STRING
This doesn't mean that on every declaration you must put either an attached mark or a detachable mark. Declarations that are unmarked are allowed. Whether unmarked declarations are considered attached or detachable is determined by the value of an EiffelStudio project setting named Are types attached by default? This setting can be set differently in different parts of your project giving you fine-grained control, which is particularly useful while converting existing software or mixing libraries of differing void-safety levels.
In Eiffel then, all declarations will have types that are either attached or detachable. As a result, we need only use CAPs and the attached syntax with detachable types. So the important thing to remember is that direct access to class attributes of detachable types is never void-safe.
When building void-safe software in Eiffel it is best, in virtually every case, to set Are types attached by default? to True. This means that if a declaration contains neither attached nor detachable, then it is assumed to be attached.
The distinction between attached and detachable types results in a small but important addition to the rules of conformance. Because variables declared as attached types can never be void, then it is important not to allow any assignment of a detachable source to an attached target. However, assigning an attached source to a detachable target is permissible. The following code shows both cases and assumes types are attached by default.
If we have attached types, then we can assume variables declared of these types, once attached, will always be attached. But how do they get attached in the first place? That's what the initialization rule is all about.
The rule says that at any place in which a variable is accessed, it must be properly set. A variable's being properly set has a precise, but not particularly simple definition in the Eiffel standard.
Info: You can find the formal definition of the Variable initialization rule, validity code VEVI, and its related concepts such as properly set variables in the ISO/ECMA standard document.
Still, it's not too hard to understand the basics of initializing variables of attached types:
create instruction, the local variable would be considered properly set. But if the create occurred in the then part of an if instruction, the local variable would not be properly set in the else part of that same if instruction:
my_routine
-- Illustrate properly set local variable
local
l_my_string: STRING
do
if my_condition then
create l_my_string.make_empty
-- ... l_my_string is properly set here
else
-- ... l_my_string is not properly set here
end
endA self-initializing attribute is guaranteed to have a value when accessed at run time. Declarations of self-initializing attributes are characterized by the use of the attribute keyword. The code that follows the attribute keyword is executed to initialize the attribute in the case that the attribute is accessed prior to being initialized in any other way.
So, self-initializing attributes are ordinary attributes, with the restriction that they are of both attached types and reference types (i.e., not expanded types or constants). Self-initializing attributes still can be, and typically will be initialized in the traditional ways. The difference is that the code in the attribute part serves as a kind of safety net guaranteeing that a self-initializing attribute will never be void, even if it is accessed prior to being initialized by one of the traditional means.
value: STRING attribute create Result.make_empty end
In the example above, the attribute value will be attached to an object of type STRING, in fact, the empty string, if no other initialization occurs before the first access of value.
You will remember that the Eiffel type system dictates that an assignment instruction:
x := yy is compatible with the type of x. Compatibility, in turn, means either conversion or conformance.
The fact that all types are either attached or detachable adds another dimension to rule for conformance:
This prevents us from circumventing attached status at run time. If x is of a detachable type, then y could be either a detachable or attached type.
The same goes for routine calls. In a call:
z.r (y)
x is the formal argument for r, then if x is of an attached type, then y must be of an attached type.
Stable attributes are really stable detachable attributes, as adding the concept of stability is meaningful only for detachable attributes. Declaring a detachable attribute as stable, means that it behaves like a detachable attribute except that its assignment rules mimic those of attached attributes. In other words, a stable attribute does not need to be attached during object creation the way that attributes declared as attached must. But like attached type attributes, stable attributes can never be the target of an assignment in which the source is Void or a detachable type.
my_test: detachable TEST note option: stable attribute end
This means that even though stable attributes do not need to be initialized like attributes of attached types, once they are attached to an object, they can never be void again.
Stable attributes are also interesting in that they are the only exception to the rule given above in the CAPs section that stated that direct access to attributes cannot be protected by a CAP. A stable attribute can be used under the protection of a CAP. This is because once a stable attribute has an object attached, it can never again be set to Void. So there's no worry about having the attribute's state going unexpectedly from attached to non-attached because of the actions of other routines or threads.
Generic classes provide another question. A generic class like
class C [G] ...
G.
So, two valid derivations are:
my_integer_derivation: C [INTEGER]
my_employee_derivation: C [EMPLOYEE]
If class C contains a declaration:
x: Gx ?
In the case of the INTEGER derivation above, we know x is safe because INTEGER is an expanded type. But often types like EMPLOYEE will be reference types which could be void at run time.
For a class like C [G], G is considered detachable. As a result, because of the rule for conformance, any class will work for an actual generic parameter. That means that both of the following are valid generic derivations:
If C contains a declaration x: G, the application of features to x must include verification of attachment (CAPs, attached syntax, etc).
Constrained genericity can be used to create generic classes in which the generic parameter represents an attached type. If class C had been defined as:
class C [G -> attached ANY] ...
x in this class G represents an attached type. Consequently, the actual generic type in any derivation must be attached ... and feature calls on x are safe.
The rule for generic parameters applies to all generic types ... except ARRAYs. In the typical creation of an ARRAY, we would provide a minimum and maximum index.
In the case of an actual generic parameter of an attached reference type, all the elements must be attached to instances of type during the creation of the ARRAY. The make procedure would not do this. Creation of an ARRAY in which the actual generic parameter is attached must be done using the make_filled creation procedure.
create my_array.make_filled ("", 1, 100)
STRING. Every entry in the newly created ARRAY will be initialized to reference this object.
For more detail on void-safe use of arrays and other generic classes, see the section: Using generic classes.
Now that we've been introduced to the Eiffel void-safe facilities, let's look at what it takes to set up a new void-safe software project. Here we'll look at the void-safety related project settings and how the can be used. Then we'll look deeper into the use of some of the void-safe tools.
Contents
|
There are three project settings that are related to void-safety. These settings can be set with great granularity throughout your project to allow you maximum flexibility, particularly when including classes or libraries that are void-unsafe or that have been converted to void-safety, but must do double duty in the void-safe and void-unsafe worlds.
The Void-safe setting determines whether and how the Eiffel compiler checks your project against the void-safe related validity rules.
This is the essential void-safe project setting. It can assume one of three values:
So, for a new void-safe project, you would want to set this option to either Complete void safety or On demand void safety.
It is this setting that tells the compiler how to treat declarations which specify neither the detachable keyword nor the attached keyword, for example:
x: Tx as if it were declared:
x: attached T
x will be viewed as if it were:
x: detachable T
In a new project, ideally all of your declarations would be of attached types. But of course there are some occasions, for various reasons, that you must or should use detachable types.
So, for a new void-safe project, it is recommended that a value of True is used.
See Also: Types as "attached" or "detachable".
This setting instructs the compiler to recheck inherited features in descendant classes.
Full class checking should always be set to True when compiling void-safe code.
As of EiffelStudio version 6.4, the majority of the libraries distributed with EiffelStudio are void-safe.
Note: During a period of transition, there will be different Eiffel configuration files (.ecf's) for void-unsafe and void-safe projects (for example, base.ecf and base-safe.ecf). If you have set the Void-safe setting to check for void-safety, then when you add a library to your project in EiffelStudio, you will see only the void-safe configurations by default. After the transition period, it is expected that there will be only one version of each of the configuration files for each library. The single configuration files will serve both void-unsafe and void-safe projects.
Void-safety affects generic classes. Fortunately, from the viewpoint of those writing clients to the generic classes in the EiffelBase library, not much has changed. Still, you should understand the interplay between void-safety and genericity.
Consider a generic class like LIST [G]. The formal generic parameter G represents an arbitrary type. In a generic derivation of LIST [G], say LIST [STRING], the formal generic type is replaced by an actual generic type, in this case STRING.
Remember that unconstrained genericity, LIST [G], for example, is really a case of constrained genericity in which the generic parameter is constrained to ANY, that is, it could be written LIST [G -> ANY].
With the advent of void-safe Eiffel, the unconstrained generic class name LIST [G] now equates to LIST [G -> detachable ANY]. Because any type, say T, (synonymous with attached T in void-safe Eiffel) conforms to detachable T, this change facilitates the production of generic classes, but has little effect on writers of clients to those classes.
This change works for all the generic classes in EiffelBase ... except for one: ARRAY. Arrays are a special case because we often create arrays with a pre-allocated number of elements. In the case of expanded types, there's not a problem. For example, in this code
my_array with one hundred INTEGER elements. INTEGER is an expanded type, and each element is initialized by applying the default initialization rule for INTEGER, i.e, the integer representation of zero.
However, if my_array had been declared of a type with reference semantics, say STRING (meaning, of course, attached STRING, the default rule would not work well, because the default initialization for references types is Void which would not be allowed in an array of elements of any attached type.
The solution to this challenge is fairly simple. For arrays of elements of detachable or expanded types, there is no different behavior. When dealing with arrays of elements of attached types, we must be careful.
Creating an array using ARRAY's creation procedure make may still be safe in some cases. Specifically, make can be used with arrays of elements of attached types if the arguments have values such that an empty array will be created, that is, when
min_index = max_index + 1
In all other situations involving arrays of elements of attached types, make may not be used to do the creation. Rather, you should use the creation procedure make_filled which takes three arguments. The first is an object of the type of the array, and the second and third are the minimum and maximum indexes, respectively. When the array is created, each of the elements will be initialized with a reference to the object of the first argument.
So, a call using make_filled would look like this:
STRING composed of one space character.
Note: Full void-safety on arrays of attached types requires a change to class ARRAY which can break existing code. During a transitional period, this and similar changes are available for testing and use in an "experimental" version of EiffelStudio. This version of EiffelStudio can be invoked by using the "-experiment" option from the command line, or by selecting it on Microsoft Windows by following the Start menu path to EiffelStudio. You are encouraged to try to compile your libraries and systems using experimental mode in order to prepare for the time when these changes are made permanent.
As of version 6.6, the normal mode of EiffelStudio becomes what had been accessible in earlier versions as "experimental".
The keyword attribute should be used with some care. You might be tempted to think that it would be convenient or add an extra element of safety to use self-initializing attributes widely. And in a way, you would be correct. But you should also understand that there is a price to pay for using self-initializing attributes and stable attributes. It is that upon every access, an evaluation of the state of the attribute must be made. So, as a general rule, you should avoid using self-initializing attributes only for the purpose of lazy initialization.
The complete attached syntax is:
attached {SOME_TYPE} exp as l_exp
In the introduction to the attached syntax, we used an example which showed how the attached syntax is directly relevant to void-safety. That is, the code:
if x /= Void then -- ... Any other instructions here that do not assign to x x.f (a) end
is a CAP for x ... but that's only true if x is a local variable or a formal argument to the routine that contains the code.
So to access a detachable attribute safely, we could declare a local variable, make an assignment, and test for Void as above. Something like this:
my_detachable_attribute: detachable MY_TYPE ... some_routine local x: like my_detachable_attribute do x := my_detachable_attribute if x /= Void then -- ... Any other instructions here that do not assign to x x.f (a) end ...
some_routine
do
if attached my_detachable_attribute as x then
-- ... Any other instructions here that do not assign to x
x.f (a)
end
...In its simplest form, the attached syntax can be used to test attached status only:
if attached x then do_something else do_something_different end
So in this simple form, attached x can be used instead of x /= Void. The two are semantically equivalent, and which one you choose is a matter of personal preference.
There is a code pattern for functions that exists in some Eiffel software to effect "once-per-object / lazy evaluation".
Note: As of EiffelStudio version 6.6, the use of this code pattern effecting "once per object" is no longer necessary. V6.6 includes explicit support for once routines which can be adjusted by a once key to specify once per object.
This "once-per-object" code pattern employs a cached value for some object which is not exported. When it is applied, the "once-per-object" function checks the attachment status of the cached value. If the cached value is void, then it is created and assigned to Result. If the cached value was found already to exist, then it is just assigned to Result.
Here's an example of this pattern used to produce some descriptive text of an instance of its class:
feature -- Access descriptive_text: STRING local l_result: like descriptive_text_cache do l_result := descriptive_text_cache if l_result = Void then create Result.make_empty -- ... Build Result with appropriate descriptive text for Current descriptive_text_cache := Result else Result := l_result end ensure result_attached: Result /= Void result_not_empty: not Result.is_empty result_consistent: Result = descriptive_text end feature {NONE} -- Implementation descriptive_text_cache: like descriptive_text
descriptive_text_cache is of an attached type, therefore will be flagged by the compiler as not properly set (VEVI). Of course, it will be ... that's the whole idea here: not to initialize descriptive_text_cache until it's actually used. So it sounds like descriptive_text_cache should be declared detachable. That is:
descriptive_text_cache: detachable like descriptive_text
l_result is typed like descriptive_text_cache, so it also will be detachable. Therefore we might expect trouble, because later in the routine we have:
Result := l_result
But we don't get such an error. The reason is two-fold. First, l_result is a local variable whose use can be protected by a CAP. Second, the CAP in this case is the check to ensure that l_result is not void. We only make the assignment to Result if l_result is not void. So the compiler can prove that l_result cannot be void at the point at which the assignment occurs ... therefore, no error.
Because the attached syntax can test attached status and provide a local variable, it can be used to remove some unnecessary code from this routine. The version of the routine that follows shows the attached syntax being used to test the attached status of descriptive_text_cache and yield the local variable l_result in the case that descriptive_text_cache is indeed attached.
descriptive_text: STRING do if attached descriptive_text_cache as l_result then Result := l_result else create Result.make_empty -- ... Build Result with appropriate descriptive text for Current descriptive_text_cache := Result end ensure result_attached: Result /= Void result_not_empty: not Result.is_empty result_consistent: Result = descriptive_text end feature {NONE} -- Implementation descriptive_text_cache: like descriptive_text
The assignment attempt ( ?= ) has traditionally been used to deal with external objects (e.g., persistent objects from files and databases) and to narrow the type of an object in order to use more specific features. The latter is a process known by names such as "down casting" in some technological circles. A classic example is doing specific processing on some elements of a polymorphic data structure. Let's look at an example. Suppose we have a LIST of items of type POLYGON:
my_polygons: LIST [POLYGON]
POLYGONs could be of many specific types, and one of those could be RECTANGLE. Suppose too that we want to print the measurements of the diagonals of all the RECTANGLEs in the list. Class RECTANGLE might have a query diagonal returning such a measurement, but POLYGON would not, for the reason that the concept of diagonal is not meaningful for all POLYGONs, e.g., TRIANGLEs.
As we traverse the list we would use assignment attempt to try to attach each POLYGON to a variable typed as RECTANGLE. If successful, we can print the result of the application of diagonal.
l_my_rectangle: RECTANGLE ... from my_polygons.start until my_polygons.exhausted loop l_my_rectangle ?= my_polygons.item if l_my_rectangle /= Void then print (l_my_rectangle.diagonal) print ("%N") end end
from my_polygons.start until my_polygons.exhausted loop if attached {RECTANGLE} my_polygons.item as l_my_rectangle then print (l_my_rectangle.diagonal) print ("%N") end end
l_my_rectangle.
check instructionsIn void-safe mode, the compiler will accept code that it can prove will only apply features to attached references at runtime ... and you help this process along by using the tools of void-safety, like attached types and CAPs. On the other hand, the compiler will reject code that it cannot guarantee is void-safe. Sometimes this may cause you a problem. There may be subtle situations in which you feel quite certain that a section of code will be free of void calls at runtime, but the compiler doesn't see it the same way, and rejects your code. In cases like this, you can usually satisfy the compiler by using check instructions.
Technically speaking, check instructions are not CAPs. But they are useful in cases in which an entity is always expected to be attached at a certain point in the code. In the following example, the attribute my_detachable_any is detachable. But at the particular point at which it is the source of the assignment to l_result, it is expected always to be attached. If it is not attached at the time of the assignment, and therefore l_result becomes void, then an exception should occur. The check instruction provides this behavior.
The following sample shows the check instruction at work. There are reasons why this is not the best use use of check in this case, and we will discuss that next.
Here the assertion in the check guarantees that l_result is attached at the time of its assignment to Result. If my_detachable_any is ever not attached to an object, then an exception will be raised.
So what's wrong with the sample above? It would be fine in workbench code, but what happens if the code is in finalized mode, in which assertions are typically discarded?
The answer is that the check in the sample above would no longer be effective, and the resulting executable would no longer be void-safe.
The solution to this problem is found in a different form of the check instruction. Consider the same example, but this time using check ... then ... end:
Here, in the improved version of the example, the check ... then ... end is used along with the attached syntax. This streamlines the code a bit by eliminating the need to declare a separate local entity, while achieving the same effect as the previous example. If my_detachable_any is attached at runtime, then the temporary variable l_result is created and attached to the same object. Then the body of the check ... then ... end is executed. If my_detachable_any is not attached, an exception occurs.
Another important benefit, one that solves the problem with the original example, comes from the way in which check ... then ... end is handled by the compiler. The check ... then ... end form is always monitored, even if assertion checking is turned off at all levels, as is usually done in finalized code.
The attached syntax is convenient because it can check attached status and deliver a new local variable at the same time. But there are cases in which you might choose instead to define a local variable and use a CAP. Suppose you had code acting on several similar and detachable expressions, and you use the attached syntax in each case:
foobar
do
if attached dictionary_entry ("abc") as l_abc then
l_abc.do_something
end
if attached dictionary_entry ("def") as l_def then
l_def.do_something
end
if attached dictionary_entry ("ghi") as l_ghi then
l_ghi.do_something
end
endfoobar, one each for l_abc, l_def, and l_ghi. And it is no better to do this:
foobar
do
if attached dictionary_entry ("abc") as l_entry then
l_entry.do_something
end
if attached dictionary_entry ("def") as l_entry then
l_entry.do_something
end
if attached dictionary_entry ("ghi") as l_entry then
l_entry.do_something
end
endfoobar.
In cases like this, you could effect a minor performance improvement by declaring one local variable and reusing it. In the following code, only one local variable is used and access to it is protected by the CAP if l_entry /= Void then.
foobar
local
l_entry: like dictionary_entry
do
l_entry := dictionary_entry ("abc")
if l_entry /= Void then
l_entry.do_something
end
l_entry := dictionary_entry ("def")
if l_entry /= Void then
l_entry.do_something
end
l_entry := dictionary_entry ("ghi")
if l_entry /= Void then
l_entry.do_something
end
endRemember that stable attributes are actually detachable attributes, with the difference that they can never be the target of an assignment in which the source is Void or anything that could have a value of Void.
Stable attributes are useful in situations in which there are valid object life scenarios in which some particular attribute will never need an object attached, or will only need an object attached late in the scenario. So in this case, the attribute is used only under certain runtime conditions. Declaring these attributes as stable eliminates the need to make attachments during object creation. Yet once needed, that is, once the attribute is attached, it will always be attached.
Also, you should remember that unlike other attributes, you can access stable attributes directly in a CAP:
my_stable_attribute: detachable SOME_TYPE note option: stable attribute end ... if my_stable_attribute /= Void then my_stable_attribute.do_something end ...
If you have been using Eiffel for a while, you may be maintaining systems which were developed before Eiffel became void-safe. If that's the case, then you will probably want to make those systems void-safe.
In this section we will use the experience of converting a set of simple (but not too simple) legacy Eiffel classes to show the types of issues that you may encounter, and how to deal with them.
So in the discussion below, you will see references to these classes:
| Class name | Description |
|---|---|
| APPLICATION | Simple root class containing declarations of types NVP and NVP_LIST |
| NVP | Class modeling name/value pairs of type STRING |
| NVP_LIST | Class modeling a list of NVP's with specialized behavior; heir of TWO_WAY_LIST [NVP] |
It's not important that you know much about the details of these classes. We will, however, look at certain parts of the classes in enough detail to resolve the conversion issues.
Contents
|
During the process of conversion of classes to void-safety, the compiler will point out problems which you will have to fix. Some of these will be straightforward, while others may be tricky. It is natural, or sometimes mandatory, at times to consider changing elements of the design of your software.
Also, as you sift through your existing software during the void-safe conversion, you may not get very far before you see things that you wish had been done differently. This occurs often during reviews of existing systems, not just because of the introduction of void-safety.
In the discussions that follow you will see these redesign opportunities arise, and the decisions that were made for these cases.
The libraries distributed with EiffelStudio have been converted to support void-safety. Mostly the changes made will cause no problems for existing software. However a few changes have been identified as "breaking" changes. You may or may not encounter the effects of these changes, but you should be aware of how they could effect your software and what options you have for adapting to them. Breaking changes are described in the EiffelStudio release notes and in the page dedicated to Void-safe changes to Eiffel libraries.
First make sure your project will compile correctly under the configuration of EiffelStudio that you intend to use to convert to void-safe.
Then set the project setting Full Class Checking to True. Do a clean compile of your system.
Full class checking will analyze your classes to make sure that in cases of inheritance, features of the parent classes are rechecked for validity in the heirs.
Here's an example of the kind of error you might expect when compiling with full class checking:
The situation here is that the feature split has been inherited (from class TWO_WAY_LIST [G]) by our class NVP_LIST. Feature split includes code to create and attach feature sublist which is typed attached like Current which in this case means attached NVP_LIST. To do this creation, split uses a creation procedure make_sublist.
Now here's the rub: NVP_LIST has not named make_sublist as a creation procedure:
create
make, make_from_string, make_from_file_namedcreate part of NVP_LIST and add make_sublist to its list of creation procedures, this will fix the problem:
create
make, make_from_string, make_from_file_named, make_sublistSo, fix any problems that arise out of turning on full class checking.
The second step in conversion of existing software is to change the values of the other void-safe related project settings and use the void-safe configurations for any delivered libraries and precompilations.
In the project settings for the target in which you are working:
Note: Remember that during a transitional period starting with V6.4, there will be multiple versions of the configuration files for Eiffel libraries and precompiles. For example, base.ecf (void-unsafe) and base-safe.ecf (void-safe). It is expected that in the future there will only be one configuration file (e.g., base.ecf) that will work with both void-safe and void-unsafe client software.
If necessary, remove Eiffel libraries and any precompiled library that your project uses and re-add them with their void-safe configuration files. Because you've set your target to void-safety, when you click Add Library, you should see only void-safe configurations by default.
You will see a check box on the dialog that you can uncheck if you want to see all available library configurations:
Now do a clean compile.
If you've replaced a precompiled library that you have not already built, EiffelStudio will offer to build it for you on the fly:
Now you should see error messages representing any situation in your project in which the compiler determines that it cannot guarantee void safety.
This is what our legacy system produced:
Next you fix the problems that the compiler discovered. The compiler errors concerning void-safety typically will be of three varieties.
Let's look at some specific cases and how fixing them unfolds.
There are two VEVI errors like this in class APPLICATION of our legacy system. They are probably the most obvious and easiest cases to handle.
feature {NONE} -- Initialization make -- Run application. do ... end feature -- Access my_nvp: NVP -- NVP for testing my_nvp_list: NVP_LIST -- NVP_LIST for testing
Here attribute declarations for my_nvp and my_nvp_list are made. These are assumed to be attached because of the project setting. But the create routine make fails to create objects and attach them. So by adding those creations, as shown below, the compiler is satisfied.
make
-- Run application.
do
create my_nvp.make ("SomeName", "SomeValue")
create my_nvp_list.make
...
end
In a second case, there is also an Initialization rule violation (VEVI), this time on Result, in this routine:
at_first (nm: STRING): NVP -- The first found NVP with name matching nm. -- Or Void if not found require nm_valid: nm /= Void and then not nm.is_empty local tc: CURSOR do tc := cursor start name_search (nm) if not exhausted then Result := item end go_to (tc) ensure index_unchanged: index = old index end
Here we cannot just ensure that Result is always attached, because, as indicated by the header comment, Result is allowed to be void by design.
So the least impact to this routine will be to declare its type as detachable:
at_first (nm: STRING): detachable NVP -- The first found NVP with name matching nm. -- Or Void if not found
The same change is made in other routines that can return void by design, particularly including a routine called value_at_first, which gets our attention next.
The case of at_first offered us an opportunity to redesign (or not). We might have been able to leave at_first attached. After all, in void-safe software, the fewer detachables, the better. Maybe we could devise a way, possibly through preconditions and other queries, that would guarantee that at_first attempts to execute only when it can return a value.
But at_first is an exported query, so a consequence of such a change in the class design is that it would affect the class interface in such a way that existing clients would have to be modified to comply. In other words, it would be a "breaking" change.
The change to at_first satisfies the VEVI issue in at_first, but it introduces a previously unseen conformance issue (VJAR) in the routine value_at_first:
value_at_first looks like this:
The problem is that the local variable tn is declared as attached, but we know that now the result of at_first is detachable, making this assignment invalid:
tn := at_first (nm)
Here the attached syntax can fix the problem and streamline the routine:
In this version tn need not be declared as a local variable. Remember that the attached syntax provides a fresh local variable, if the expression is not void.
A design issue in class NVP_LIST causes both conformance and initialization compiler errors. In the original design, an instance of the class NVP_LIST could traverse its contents NVP-by-NVP with inherited functionality. Additionally, NVP_LIST has immediate functionality allowing an instance to traverse its contents in two different ways returning "sublists" based on recurring patterns of the name attributes of a sequence of name/value pairs.
These two traversal methods are referred to as "sequencing" and "segmenting". It's not important that you understand the details of what these traversals do. But it is important to know that a valid instance of NVP_LIST can either be in the process of sequencing or in the process of segmenting, or neither. It is invalid to be both sequencing and segmenting.
Two class attributes are maintained to store the recurring patterns of values of {NVP}.name that guide traversal:
In the original class design, each of these attributes would be void unless their corresponding traversal was active. So the class contains the following clauses in its invariant:
not_sequencing_and_segmenting: not (segment_readable and sequence_readable) sequence_traversal_convention: (sequence_array = Void) = (not sequence_readable) segment_traversal_convention: (segment_array = Void) = (not segment_readable)
Of course by default these attributes are considered to be attached. So, because they are not initialized during creation, we see initialization errors. Because elements of the class intentionally set them to Void, we see conformance errors.
Here we have another opportunity to redesign (or not). We could mark the two arrays as detachable, recompile and fix any problems this causes (in fact, it causes eight errors: six Target rule violations, and two conformance issues).
However, because these attributes are not exported, we may be able to leave them attached and make changes to the implementation design without making breaking changes to the interface.
Those exported features which take arguments of the type ARRAY [STRING] which will serve as sequencing or segmenting control also require that the array contain at least one element. For example, the contract for feature segment_start contains these preconditions:
segment_start (nms: ARRAY [STRING_8]) -- Place segment cursor on the first occurrence of a seqment of list which -- begins at the current cursor position and -- terminates in a sequence with names equivalent to and ordered the same as `nms'. -- If no such sequence exists, then ensure exhausted require nms_valid: nms /= Void and then (nms.count > 0) not_sequencing: not sequence_readable
Because the restriction always exists that a valid sequence_array or segment_array must contain at least one element, it is possible to redesign the implementation of the class such that an empty sequence_array and segment_array could serve the same purpose as a Void one does in the original design.
So the invariant clauses that we saw above would now become:
not_sequencing_and_segmenting: not (segment_readable and sequence_readable) sequence_traversal_convention: (sequence_array.is_empty) = (not sequence_readable) segment_traversal_convention: (segment_array.is_empty) = (not segment_readable)
We already have compiler errors (VJAR's) that point us to those places in which we have code that sets either sequence_array or segment_array to Void. Like this:
segment_array := Void
These instances need to be changed to attach an empty array, maybe like this:
create segment_array.make (1, 0)
Additionally, some postconditions which reference the implementation features sequence_array and/or segment_array would have to be changed. Looking at the postcondition clauses for segment_start we see that segment_array is expected (or not) to be Void:
ensure started: (not exhausted) implies (segment_readable and (segment_array /= Void) and (last_segment_element_index > 0)) not_started: exhausted implies ((not segment_readable) and (segment_array = Void) and (last_segment_element_index = 0))
To support the "empty array" design, segment_start's postcondition clauses would be:
ensure started: (not exhausted) implies (segment_readable and (not segment_array.is_empty) and (last_segment_element_index > 0)) not_started: exhausted implies ((not segment_readable) and (segment_array.is_empty) and (last_segment_element_index = 0))
See Also:
Converting EiffelVision 2 Systems to Void Safety
Void-safe changes to Eiffel libraries
Contents
|
During the adoption of void-safety, the software libraries provided by Eiffel Software have been converted to be void-safe. The bulk of the changes made to these libraries will have little or no adverse effect on your existing software as you go through the process of void-safe conversion. However, there are a few changes to the library that we consider "breaking" changes, that is, important changes that might cause problems in existing systems that use certain library classes.
Note: Many of these changes were in effect in the experimental mode of versions 6.4 and 6.5. With the release of version 6.6, the experimental mode of previous versions became the default mode and, consequently may have caused these changes to become more apparent to some users. A compatibility mode is available to ease transition. The compatibility mode is accessible using the -compat command line option or through the EiffelStudio choices provided through the Microsoft Windows Start button.
ARRAYSome additional preconditions are in force in ARRAY in void-safe mode.
In void-unsafe mode, the behavior is equivalent to that of previous versions.
make_from_specialThe signature of this routine has changed.
Current signature: make_from_special (a: SPECIAL [G])
Previous signature: make_from_special (a: SPECIAL [G]; min_index, max_index: INTEGER)
Using the current version will create an array with a range from 1 to the number of elements in the argument `a'.
auto_resizeThis implementation (private) feature has been removed.
ARRAYED_LISTPreviously ARRAYED_LIST conformed to ARRAY. This is no longer the case. The feature {ARRAYED_LIST}.to_array can be used to produce an instance of ARRAY from an instance of ARRAYED_LIST.
count and areaPreviously these two queries were attributes. They are now functions.
HASH_TABLEThe internal implementation has changed in ways that cause the order of traversal to differ from previous versions.
SPECIAL and TO_SPECIAL{SPECIAL}.makeThis void-unsafe feature has been removed.
In its place, the creation procedures {SPECIAL}.make_filled and {SPECIAL}.make_empty can be used.
{SPECIAL}.make_filled is available in both default and compatible modes. Use this creation procedure if you want code that is compatible with both modes.
{SPECIAL}.make_empty is available in default mode only.
{TO_SPECIAL}.make_areaIn order to reflect the above change to class SPECIAL, the make_area feature of TO_SPECIAL has been removed in favor of {TO_SPECIAL}.make_filled_area and {TO_SPECIAL}.make_empty_area.
The availability of {TO_SPECIAL}.make_filled_area and {TO_SPECIAL}.make_empty_area corresponds to that noted above for the creation features of SPECIAL:
{TO_SPECIAL}.make_filled_area is available in both default and compatible modes. Use make_filled_area for code that needs to compile in both modes.
{TO_SPECIAL}.make_empty_area is available only in default mode.
{SPECIAL}.count to {SPECIAL}.capacityIn previous versions, for a particular instance of SPECIAL the queries count and capacity yielded the same value.
This is no longer always true. If an instance of SPECIAL is created with, for example, make_empty (10), although the capacity will be 10, the count will be zero.
However creating a SPECIAL using make_filled will produce an instance in which count and capacity are equal upon creation. So the behavior is similar to that of previous versions. Also, make_filled is available in both default and compatible modes.
If your code depends upon count and capacity having the same value, then you can use make_filled for creation. And then if you need resizing, use the "_with_default" versions of the "resized" features, specifically resized_area_with_default and aliased_resized_area_with_default.
Under construction: what it means.
Eiffel Software recommends that any new development efforts be implemented using Eiffel's void-safe approach, thus eliminating one more common type of runtime failure. It is also recommended that existing software be converted to void-safety at the earliest opportunity.
Under some circumstances it is possible and even helpful to mix void-safe and void-unsafe libraries. During conversion to void-safety, for example, it can be helpful to compile and test a void-unsafe system with void-safe versions of the libraries it depends upon.
The rule for using void-safe and void-unsafe software together is fairly simple.
Rule -- Mixing void-safe and void-unsafe software:
1) A class that is void-unsafe may depend upon other classes (as suppliers or ancestors) which are either void-safe or void-unsafe.
2) A class that is void-safe may depend only upon other classes that are void-safe.
This means that if the root class of a system is void-safe, then every other class in the system must also be void-safe.
However, if you are converting a system to void-safety, it's likely that your root class and the classes in the closely related clusters will be void-unsafe. The rule allows you to mix the void-safe versions of the Eiffel Software library classes from the EiffelStudio distribution with your void-unsafe system during conversion.
Contents
|
Certified Attachment Patterns (CAPs) were described in the section on void-safety tools. To review, a CAP is a code pattern for a certain expression, say exp of a detachable type that ensures that exp will never have a void run-time value within the covered scope.
A simple example is the familiar test for void reference:
if l_x /= Void then l_x.do_something -- Valid for formal arguments, local variables, and stable attributes end
l_x is not Void, that the feature application l_x.do_something is void-safe.
Of course, you should remember from previous discussions that l_x must be a local variable, a formal argument, or a stable attribute.
When void-safety was first envisioned for Eiffel, it was intended that individual CAPs would be proven or certified and documented. This would produce a "catalog" of CAPs.
What happened instead is that the members of the Eiffel standard committee have been able to produce and publish as part of the standard a definition of the nature of a CAP from which a determination can be made as to whether a particular code pattern is or is not a CAP.
The definition in the standard document is not easily readable by most developers. So, in this documentation, you will see various examples of CAPs and the rationale behind them.
The Eiffel standard (2nd edition, June 2006) defines a CAP as follows:
A Certified Attachment Pattern (or CAP) for an expression exp whose type is detachable is an occurrence of exp in one of the following contexts:
1. exp is an Object-Test Local and the occurrence is in its scope.
2. exp is a read-only entity and the occurrence is in the scope of a void test involving exp.
The terminology used in the definition is precise. For example, terms like "read-only entity" and "scope of a void test" have specific meanings that are supported by their own definitions in the standard.
Still, the standard does contain informative text that gives us a guideline that a CAP is a scheme to ensure that a particular expression of a detachable type will never have void run-time value in the scope covered by the CAP.
The discussion here will follow that guideline, and, as such, will be less formal (and consequently less precise) than that in the standard, and is intended to be a practical guide. Of course, the standard document is available for download if you wish to investigate the specifics.
In the first context in the definition above, the expression exp can be an Object-Test Local. An Object-Test Local is the identifier specified for a fresh local entity in an object test. Remember, object tests are coded using the attached syntax.
attached x as l_x
l_x is an Object-Test Local.
In the second context, the expression can be a read-only entity. Read-only entities are:
Current
Additionally, the Eiffel Software compiler allows for stable attributes and local variables to be protected by a CAP.
Stable attributes are the only class attributes which are CAP-able. This is because stable attributes, once attached at run-time, can never have a void value again. So, you use stable attributes safely by using them under the protection of a CAP. Consider this stable attribute:
my_stable_string: detachable STRING note option: stable attribute end
my_stable_string, because it is stable, is not required to be initialized during the creation of instances of the class in which it is a feature. That means that for each instance, my_stable_string can be initialized later during the instance's life-cycle or not at all. But because it is detachable, my_stable_string cannot be accessed in any context in which it cannot be determined that it is currently attached. For ordinary attributes, this means either using an object test and accessing the object through an object test local, or using using a local variable under the protection of a CAP.
Stable attributes however, can be used directly in a CAP, as shown below:
if my_stable_string /= Void then my_stable_string.append ("abc") -- Valid ...
So using stable attributes can reduce the need to initialize rarely used attributes, and the need to code object tests.
Local variables can be used in a CAP as long as they are not the target of an assignment whose source is Void or some expression which could possibly be void.
So, for a local variable l_string, the following is valid:
local l_string: detachable STRING do if l_string /= Void then l_string.append ("abc") -- Valid ...
But, if l_string had been a target of an assignment in which the source could possibly have been void, then it could no longer be guaranteed that l_string is not void. So, assuming that my_detachable_string is an attribute declared as type detachable STRING, the second application of append in this example would be invalid:
local l_string: detachable STRING do if l_string /= Void then l_string.append ("abc") -- Valid l_string := my_detachable_string l_string.append ("xyz") -- Invalid: my_detachable_string might have been void ...
We've already seen the simple test for void as a CAP:
local l_str: detachable STRING ... if l_str /= Void then l_str.append ("xyz") -- Valid end
Additionally, a creation instruction can serve as a CAP. After the execution of a creation instruction, the target of the creation instruction will be attached:
local l_str: detachable STRING do create l_str.make_empty l_str.append ("xyz") -- Valid ...
There are some situations that constitute CAPs that we might not think of immediately.
For example, the case of the non-strict boolean operator and then:
if x /= Void and not x.is_empty then -- Invalid ... if x /= Void and then not x.is_empty then -- Valid ...
x is CAP-able, the first line of code is invalid because the expression x.is_empty could always be evaluated even in the case in which x is void.
In the second line of code, the non-strict boolean is used, guaranteeing that x.is_empty will not be evaluated in cases in which x is void. Therefore, x.is_empty falls within the scope of the void test on x.
In contracts, multiple assertion clauses are treated as if they were separated by and then. This allows preconditions like the one in the following example:
my_routine (l_str: detachable STRING) require l_str /= Void not l_str.is_empty -- Valid ...
Another not-so-obvious CAP is related to the use of the logical implication:
local l_str: detachable STRING do if l_str /= Void implies some_expression then ... else l_str.append ("xyz") -- Valid end
In summary, CAPs provide void-safe protection for certain types of detachable expressions.
Possibly the characteristic of CAPs which is most important to developers is whether or not a particular CAP is supported by the compiler. In other words, from the developers viewpoint, the only opinion that matters in the argument of whether a particular pattern constitutes a CAP is that of the compiler.
If the compiler can provide assurance that a certain code pattern guarantees void-safe protection, then the developer will have that pattern available as a CAP. Likewise, even if a pattern can be shown logically to be a CAP, but for some reason it is not supported by the compiler, then that pattern will not available as a CAP and the compiler will not allow its use.
The Quick Reference to the Eiffel programming language provides an informal guide to the syntax and reserved words of the language. The Eiffel programming language is described in detail in the ISO/ECMA standard document, available online.
Sometimes there are differences between the language as defined by the standard and that which is implemented by Eiffel Software. These differences are documented in the online documentation.
So, the final authority on Eiffel as implemented by Eiffel Software is the content of the standard document, amended by those variances cited in the "differences" chapter of the online documentation.
This reference is based on the June 2006 ISO/ECMA standard document.
The syntax specification shown here is a less complete and less formal version of that which is in the Eiffel ISO/ECMA standard document. The format is BNF-E. The Language Specification section of the standard document includes an overview of BNF-E.
There are a few parts of the syntax that are either non-production or non-representable in BNF-E. Some of these have been omitted from the following specification. These omitted parts of the syntax definition add to the precision of the specification, but knowledge of them is not always vital for developers.
In the BNF-E representation, generally non-terminals which are defined in the same group of productions in which they are used are not linked. However when a non-terminal is defined outside a group in which it is used, it is linked to the group in which it is defined.
The following section contains those non-production elements of the specification that are used later in the BNF-E specification.
An identifier is a sequence of one or more alphanumeric characters of which the first is a letter.
The definition is augmented by the rule that Identifiers are not valid if they are the same as one of the language's reserved words.
Characters are either:
A real -- specimen of Real -- is made of the following elements, in the order given:
No intervening character (blank or otherwise) is permitted between these elements. The integral and fractional parts may not both be absent.
A string -- specimen of construct String -- is a sequence of zero or more manifest characters.
A simple string -- specimen of Simple_string -- is a String consisting of at most one line (that is to say, containing no embedded new-line manifest character), possibly containing codes for special characters.
| Character | Code | Mnemonic name |
|---|---|---|
| @ | %A | At-sign |
| BS | %B | Backspace |
| ^ | %C | Circumflex |
| $ | %D | Dollar |
| FF | %F | Form feed |
| \ | %H | Backslash |
| ~ | %L | Tilde |
| NL (LF) | %N | Newline |
| ` | %Q | Backquote |
| CR | %R | Carriage return |
| # | %S | Sharp |
| HT | %T | Horizontal tab |
| NUL | %U | Null |
| | | %V | Vertical bar |
| % | %% | Percent |
| ' | %' | Single quote |
| " | %" | Double quote |
| [ | %( | Opening bracket |
| ] | %) | Closing bracket |
| { | %< | Opening brace |
| } | %> | Closing brace |
A sequence of characters consisting of the following, in order:
Line wrapping parts are used as separators between one Simple_string and the next in a Basic_manifest_string so that the string can be split across lines.
Class_name ::= Identifier
Class_declaration ::= [Notes]
Class_header
[Formal_generics]
[Obsolete]
[Inheritance]
[Creators]
[Converters]
[Features]
[Invariant]
[Notes]
end
Notes ::= note Note_list
Note_list ::= {Note_entry ";" …}*
Note_entry ::= Note_name Note_values
Note_name ::= Identifier ":"
Note_values ::= {Note_item ","…}+
Note_item ::= Identifier | Manifest_constant
Class_header ::= [Header_mark] class Class_name
Header_mark ::= deferred | expanded | frozen
Obsolete ::= obsolete Message
Message ::= Manifest_string
Features ::= Feature_clause+
Feature_clause ::= feature [Clients] [Header_comment] Feature_declaration_list
Feature_declaration_list ::= {Feature_declaration ";" …}*
Header_comment ::= Comment
Feature_declaration ::= New_feature_list Declaration_body
Declaration_body ::= [Formal_arguments] [Query_mark] [Feature_value]
Query_mark ::= Type_mark [Assigner_mark]
Type_mark ::= ":" Type
Feature_value ::= [Explicit_value]
[Obsolete]
[Header_comment]
[Attribute_or_routine]
Explicit_value ::= "=" Manifest_constant
New_feature_list ::= {New_feature "," …}+
New_feature ::= [frozen] Extended_feature_name
Attribute_or_routine ::= [Precondition]
[Local_declarations]
Feature_body
[Postcondition]
[Rescue]
end
Feature_body ::= Deferred | Effective_routine | Attribute
Extended_feature_name ::= Feature_name [Alias]
Feature_name ::= Identifier
Alias ::= alias '"' Alias_name '"' [convert]
Alias_name ::= Operator | Bracket
Bracket ::= "[ ]"
Operator ::= Unary | Binary
Unary ::= not | "+" | "–" | Free_unary
Binary ::= "+" | "–" | "*" | "/" | "//" | "\\" | "^" | ".." |
"<" | ">" | "<=" | ">=" |
and | or | xor | and then | or else | implies |
Free_binary
Note: Free_unary and Free_binary are free operators that are distinct from (respectively) the standard unary and binary operators (one- or two-character symbols) explicitly listed in the Unary and Binary productions. See Definition: Free operator in the standard for more precision.
Assigner_mark ::= assign Feature_name
Inheritance ::= Inherit_clause+
Inherit_clause ::= inherit [Non_conformance] Parent_list
Non_conformance ::= "{" NONE "}"
Parent_list ::= {Parent ";" …}+
Parent ::= Class_type [Feature_adaptation]
Feature_adaptation ::= [Undefine]
[Redefine]
[Rename]
[New_exports]
[Select]
end
Rename ::= rename Rename_list
Rename_list ::= {Rename_pair "," …}+
Rename_pair ::= Feature_name as Extended_feature_name
Clients ::= "{" Class_list "}"
Class_list ::= {Class_name "," …}+
New_exports ::= export New_export_list
New_export_list ::= {New_export_item ";" …}+
New_export_item ::= Clients [Header_comment] Feature_set
Feature_set ::= Feature_list | all
Feature_list ::= {Feature_name "," …}+
Formal_arguments ::= "(" Entity_declaration_list ")"
Entity_declaration_list ::= {Entity_declaration_group ";" …}+
Entity_declaration_group ::= Identifier_list Type_mark
Identifier_list ::= {Identifier "," …}+
Deferred ::= deferred
Effective_routine ::= Internal | External
Internal ::= Routine_mark Compound
Routine_mark ::= do | Once
Once ::= once [ "("Key_list ")" ]
Key_list ::= {Manifest_string "," …}+
Local_declarations ::= local [Entity_declaration_list]
Compound ::= {Instruction ";" …}*
Instruction ::= Creation_instruction | Call | Assignment | Assigner_call | Conditional | Multi_branch
| Loop | Debug | Precursor | Check | Retry
Precondition ::= require [else] Assertion
Postcondition ::= ensure [then] Assertion [Only]
Invariant ::= invariant Assertion
Assertion ::= {Assertion_clause ";" …}*
Assertion_clause ::= [Tag_mark] Unlabeled_assertion_clause
Unlabeled_assertion_clause ::= Boolean_expression | Comment
Tag_mark ::= Tag ":"
Tag ::= Identifier
Old ::= old Expression
Only ::= only [Feature_list]
Check ::= check Assertion [Notes] end
Variant ::= variant [Tag_mark] Expression
Precursor ::= Precursor [Parent_qualification] [Actuals]
Parent_qualification ::= "{" Class_name "}"
Redefine ::= redefine Feature_list
Undefine ::= undefine Feature_list
Type ::= Class_or_tuple_type | Formal_generic_name | Anchored
Class_or_tuple_type ::= Class_type | Tuple_type
Class_type ::= [Attachment_mark] Class_name [Actual_generics]
Attachment_mark ::= "?" | "!"
Anchored ::= [Attachment_mark] like Anchor
Anchor ::= Feature_name | Current
Actual_generics ::= "[" Type_list "]"
Type_list ::= {Type "," …}+
Formal_generics ::= "[" Formal_generic_list "]"
Formal_generic_list ::= {Formal_generic ","…}+
Formal_generic ::= [frozen] Formal_generic_name [Constraint]
Formal_generic_name ::= [?] Identifier
Constraint ::= "–>" Constraining_types [Constraint_creators]
Constraining_types ::= Single_constraint | Multiple_constraint
Single_constraint ::= Type [Renaming]
Renaming ::= Rename end
Multiple_constraint ::= "{" Constraint_list "}"
Constraint_list ::= {Single_constraint "," …}+
Constraint_creators ::= create Feature_list end
Tuple_type ::= TUPLE [Tuple_parameter_list]
Tuple_parameter_list ::= "[" Tuple_parameters "]"
Tuple_parameters ::= Type_list | Entity_declaration_list
Manifest_tuple ::= "[" Expression_list "]"
Expression_list ::= {Expression "," …}*
Converters ::= convert Converter_list
Converter_list ::= {Converter ","…}+
Converter ::= Conversion_procedure | Conversion_query
Conversion_procedure ::= Feature_name "(" "{" Type_list "}" ")"
Conversion_query ::= Feature_name ":" "{" Type_list "}"
Select ::= select Feature_list
Conditional ::= if Then_part_list [Else_part] end
Then_part_list ::= {Then_part elseif …}+
Then_part ::= Boolean_expression then Compound
Else_part ::= else Compound
Multi_branch ::= inspect Expression [When_part_list] [Else_part] end
When_part_list ::= When_part+
When_part ::= when Choices then Compound
Choices ::= {Choice "," …}+
Choice ::= Constant | Manifest_type | Constant_interval | Type_interval
Constant_interval ::= Constant ".." Constant
Type_interval ::= Manifest_type ".." Manifest_type
Loop ::= Initialization
[Invariant]
Exit_condition
Loop_body
[Variant]
end
Initialization ::= from Compound
Exit_condition ::= until Boolean_expression
Loop_body ::= loop Compound
Debug ::= debug [ "("Key_list ")" ] Compound end
Attribute ::= attribute Compound
Entity ::= Variable | Read_only
Variable ::= Variable_attribute | Local
Variable_attribute ::= Feature_name
Local ::= Identifier | Result
Read_only ::= Formal | Constant_attribute | Current
Formal ::= Identifier
Constant_attribute ::= Feature_name