Covariance through renaming
Warning: Warning: Article under development
Covariant arguments are part of the Eiffel language, introducing the known problems of CAT calls and global analysis. This page summarizes how to solve a classic covariant problem purely though the well-known and understood renaming mechanism in Eiffel. We will see that this solution is more verbose than the covariant solution, but has a set of advantages, including the explicit need to rework the contract, define handlers for CAT calls or the explicit creation of exceptions.
Contents
Example
A class ANIMAL defines a command called 'eat' with an argument 'food':
class ANIMAL feature -- Access last_food: FOOD feature -- Eating eat (f: FOOD) is require not_void: f /= Void do last_food := f ensure last_food = f end end
And we introduce the food class:
interface class FOOD feature -- Access is_vegetarian: BOOLEAN end
Now we want to model the fact that cows only eat grass, a vegetarian food.
class COW inherit ANIMAL redefine eat end feature -- Eating eat (g: GRASS) is do last_food := g end invariant only_eats_vegetarian: last_food.is_vegetarian end
interface class GRASS inherit FOOD invariant grass_is_vegetarian: is_vegetarian end
We have a potential CAT call, as we can regard a COW as an ANIMAL. An ANIMAL can be fed with any food:
local a_cow: COW a_animal: ANIMAL do create a_cow an_animal := a_cow an_animal.eat (create {FOOD}) -- CAT call! end
What is the origin of the CAT call? The error comes from strengthening of the precondition of eat through the type system. The precondition in ANIMAL states something like "I will eat everything, as long as it is FOOD", while the precondition in COW states: "I will eat everything, as long as it is GRASS." - so we are faceing a miss-use of the inheritance relation, as a COW is not an ANIMAL - at least not when it comes to food consumption.
It is a very typical problem of object-oriented modelling, as the subtype relation not only describes a 'COW is an ANIMAL' (observation) but also a 'what we can do to an ANIMAL, we can also to to a COW' (modification) relation.
Solution 1: explicit exception
We start by clearly stating through the code when a CAT call happens. This solution does not require any further thinking and could be regarded as a flattened form of covariance (though this should be rejected, as this would take away the benefits of making exceptions explicit).
class COW inherit ANIMAL rename eat as animal_eat redefine animal_eat end feature -- Eating eat (g: GRASS) is require not_void: g /= Void do last_food := g ensure last_food = f end animal_eat (f: FOOD) is local g: GRASS do g ?= f if g /= Void then eat (g) else raise (1,"CAT call") end end invariant only_eats_vegetarian: last_food.is_vegetarian end
The advantage of this solution is that it does clearly show that it is possible to get an exception through calling 'animal_eat' in COWs. It does not help preventing the exception from occuring, but it shows that there is a deficiency in the code that needs to be taken care of.
Solution 2: stronger precondition
The deeper insight is that ANIMAL is not a correct model of an animal. The contract is too weak: we know from reality that not every animal will eat every food. Animals are picky. We model this by a test predicate that checks if the animal will actually like our food.
class ANIMAL feature -- Access last_food: FOOD feature -- Status report likes (f: FOOD): BOOLEAN is require not_void: f /= Void do Result := True end feature -- Eating eat (f: FOOD) is require likes (f) do last_food := f ensure last_food = f end end
class COW inherit ANIMAL rename eat as animal_eat redefine animal_eat, likes end feature -- Status report likes (f: FOOD): BOOLEAN is local g: GRASS do g ?= f Result := (g /= Void) end feature -- Eating eat (g: GRASS) is do last_food := g end animal_eat (f: FOOD) is local g: GRASS do g ?= f -- We can assume that this will succeed from the local definition of `likes'. eat (g) end invariant only_eats_vegetarian: last_food /= Void implies last_food.is_vegetarian end
Discussion
From the perspective of the interface, the covariant and the renaming solutions all have the same interface, with the exception that in the renaming cases, COW also has an `animal_eat' command that it inherits from ANIMAL. Though this may seem unwanted, it makes the generation of flattened forms easier and also shows that the 'eat' of ANIMAL and 'eat' of COW are not the same features.
Also, we do not address the problems of CAT calls through genericity and change of export status. These problems need a different treatment (for example though the restriction of the interface though readonly types).