Ideally, an object-oriented language treats all values as instances of some class. We have encountered this uniform treatment in Python and Smalltalk, for example. However, the approach taken by these languages comes at a high price. Even elementary value such as integers and doubles are always wrapped into an object with the associated memory and performance overhead. Moreover, we sometimes expect different semantics for different kinds of values. Integers, for example, should have value semantics: When we assign an integer variable to another, we expect the integer value to be copied rather than just a reference to an integer object.
Objective C and C++ keep the types inherited from C as they are. This way, they don't have to pay object overhead, but also loose the uniform treatment of values. The semantics of assignment and comparison depend on whether we use pointers to objects or the objects themselves. In C++, we have complete control over where the object lives (stack or heap) and how to access it (pointer or value).
From what we have seen for now, Eiffel seems to be doing the right thing. All values are objects, that is, instances of classes. An integer is an instance of the INTEGER class defined in the standard library. We can access features (e.g., the to_string method) of elementary types just like of any other class.
The semantics, on the other hand, change according to the type. Elementary types such as integers and double expose value semantics whereas the class we defined ourselved showed reference semantics. Eiffel accomplishes this using the notion of an expanded type. Using an expanded type implies value semantics for assignment. Physically, the objects are allocated effiently on the stack. We can either define a whole class as an expanded type or single variable. The elementary type such as INTEGER and DOUBLE are all defined as expanded classes. Here is an example of an expanded class of our own modeling pairs of doubles.
expanded class DOUBLE_PAIR feature make(a_first, a_second: DOUBLE) is do first := a_first second := a_second end to_string: STRING is do Result := "(" + first.to_string + ", " + second.to_string + ")" end first, second: DOUBLE end
In a client program, we can now use the expanded class just like a built-in expanded class.
class DOUBLE_PAIR_TEST creation make feature make is local a: DOUBLE_PAIR do a.make(2, 3) print("a=" + a.to_string + "%N") end end
The variable a is not a reference to an object, but refers to the pair directly. Like an integer variable, the pair is automatically initialized to the default value (a pair of zeros).
Exception handling is another area where Eiffel adds an interesting twist. In other object-oriented languages we are free to raise and catch exceptions almost anywhere in the code. We can ignore an exception with an empty catch clause, use exceptions to implement conditional logic (in which case they become go-to statements in disguise).
As it turns out, exceptions are most properly used for the truely exceptional; events which are unexpected and interrupt the normal flow. Eiffel takes this position and supports only two ways to handle an exception: Either we can retry the affected operation or it fails.
Syntactically, this implies that a method has at most one exception handling block, which Eiffel calls the rescue clause. It is the last clause of an operation (after the postconditions which could raise exceptions as well). The following example merely demonstrates the syntax and should not be taking as a good example for the use of exceptions, since incorrect input is not unexpected and the same logic can be achieved with a simple loop.
class TEST inherit EXCEPTIONS creation make feature make is local i: INTEGER do print("enter positive integer: ") std_input.read_integer i := std_input.last_integer check i>0 end print("i=" + i.to_string + "%N") rescue print("exception=" + exception.to_string) if exception = Check_instruction then std_input.skip_remainder_of_line retry end end end
In the "main success scenario", we ask the user to enter a positive number, he or she does so, and we print the entered number. We use an assertion to check that the entered number is positive. If this assertion fails, it raises an exception. The exception is caught in the rescue clause which first print the exception number.
Eiffel's exceptions bear more resemblance with good old error codes than the exception objects found in newer languages. By inheriting from EXCEPTIONS, we have access to the error code in form of the exception feature. The EXCEPTIONS class also contains constants for the codes of the core exceptions. If the exception was raised by our check instruction, we skip the remaining input and try again using the retry command. Otherwise, we don't do anything which means that the operation fails (the exception is rethrown). We can test this behavior by interrupting the program (Ctrl-C on UNIX).
The only difference between an ordinary method and an operator is the name which consists of one of the two keywords infix or prefix followed by the operator string. Here is an example defining the plus operator for pairs of doubles.
infix "+" (other: DOUBLE_PAIR): DOUBLE_PAIR is do Result.make(first + other.first, second + other.second) end
Once defined, we can use the operator just like a built-in one.
class DOUBLE_PAIR_TEST creation make feature make is local a, b: DOUBLE_PAIR do a.make(2, 3) b.make(3, 4) print("a+b=" + (a+b).to_string + "%N") end end
Having seen the complex template syntax of C++, generic types are surprisingly simple in Eiffel. Here is an example using the built-in array type.
class ARRAY_TEST creation make feature make is local v: ARRAY[DOUBLE] i: ITERATOR[DOUBLE] do create v.make(0, 3) v.put(1.5, 1) print("v[1]=" + v.item(1).to_string + "%N") i := v.get_new_iterator from i.start until i.is_off loop print("value=" + i.item.to_string + "%N") i.next end end end
What the angle brackets are for generic types in the C family, square brackets are in Eiffel. First, we declare an array and an iterator of doubles. We create an array with four elements indexed from zero to three. The put method lets us set individual elements in the array, and the item method is used to read them. Alternatively we can call the @ operator.
print("v @ 1=" + (v @ 1).to_string + "%N")
Like any collection (that is, class derived from COLLECTION), arrays provide an iterator with the get_new_iterator method. Eiffel's iterators use a more conventional API than the standard template library of C++.
As an example of our own generic type, let's generalize our pair class to arbitrary element types.
class PAIR[G] creation make feature make(a_first, a_second: G) is do first := a_first second := a_second end to_string: STRING is do Result := "(" + first.to_string + ", " + second.to_string + ")" end first, second: G end
We just add the type parameter [G] to the class name and replace all occurences of DOUBLE by the type parameter G. With the new generic type, the test program looks as follows:
class PAIR_TEST creation make feature make is local a: PAIR[DOUBLE] do create a.make(1.5, 2.5) print("a=" + a.to_string + "%N") end end
In the preceding chapters we saw many examples of higher order functions, that is, situations where a function or even just a block of code is treated as an object which is passed to another function. Strongly-typed object-oriented languages seem to have some difficulty with this concept. In Eiffel, a (bound) method is turned into an object using the agent instruction. The resulting object is either a FUNCTION or a PROCEDURE, and these two generic classes offer method to execute the method contained in the agent. As usual, it is best to see a small example first.
class TEST creation make feature make is do apply(agent add, 44, 55) end add(x: DOUBLE; y: DOUBLE): DOUBLE is do Result := x + y end apply(f: FUNCTION[ANY, TUPLE[DOUBLE, DOUBLE], DOUBLE] x: DOUBLE; y: DOUBLE) is local z: DOUBLE do z := f.item([x, y]) print("f(" + x.to_string + ", " + y.to_string + ")=") print(z.to_string + "%N"); end end
The most complicated part is the signature of the apply method. The first argument f is declared as a function (in Eiffel a method returning a value) which belongs to any class (any class derived from ANY), takes two doubles as arguments and returns a double.
In other words, FUNCTION is a generic type with three type parameters. The first type parameter if the class the method to be wrapped by the FUNCTION belongs to. The other two type parameters describe the signature of the method. The second type parameter is the tuple of argument types, and the third type parameter the return type.
This also explains how the function argument is applied to the other two arguments x and y.
z := f.item([x, y])
The FUNCTION object has a method item which takes the arguments as a tuple, applies the wrapped function, and returns the result. Together with the agent instruction which turns a method magically into a function object, we can implement higher order functions.