14.3. More Features

14.3.1. Expanded Types

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

14.3.2. Exceptions

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

14.3.3. Operator Overloading

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

14.3.4. Generic Types

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

14.3.5. Agents

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.