In some cases, however, one type can substitute for the other type but with a few exceptions. There are several ways to deal with this problem when it occurs.
[This section attempts to provide some insight into dealing with covariance. It is not essential to understanding the language, but might help in the design of your type hierarchy.]
We will consider the definition of an animal class, where both herbivores and carnivores are animals.
abstract class $ANIMAL is eat(food:$FOOD); ... abstract class $HERBIVORE < $ANIMAL is ... abstract class $CARNIVORE < $ANIMAL is ... |
The problem is similar to that in the previous section, but is different in certain ways that lead to the need for different solutions
The ideal solution would be to do what we did in the previous section - realize the conceptual problem and rearrange the type hierarchy to be more accurate. There is a difference in this case, though. When considering omnivores, the 'eat' operation was central to the definition of the subtyping relationship. In the case of animals, the eat operation is not nearly as central - the subtyping relationship is determined by many other features, completely unrelated to eating. It would be unreasonable to force animals to be subtypes of carnivores or herbivores.
A simple solution would be to determine whether we really need the 'eat' routine in the animal class. In human categories, it appears that higher level categories often contain features that are present, but vary greatly in the sub-categories. The feature in the higher level category is not "operational" in the sense that it is never used directly with the higher level category. It merely denotes the presence of the feature in all sub-categories.
Since we do not know the kind of food a general animal can eat, it may be reasonable to just omit the 'eat' signature from the definition of $ANIMAL. We would thus have:
Another solution, that should be adopted with care, is to permit the 'eat($FOOD)' routine in the animal class, and define the subclasses to also eat any food. However, each subclass dynamically determines whether it wants to eat a particular kind of food.
abstract class $ANIMAL is eat(arg:$FOOD); ... abstract class $HERBIVORE < $ANIMAL is ... -- supports eat(f:$FOOD); class COW < $HERBIVORE is eat(arg:$FOOD) is typecase arg when $PLANT then ... -- eat it! else raise "Cows only eat plants!"; end; end; end; |
The 'eat' routine in the COW class accepts all food, but then dynamically determines whether the food is appropriate i.e. whether it is a plant.
This approach carries the danger that if a cow is fed some non-plant food, the error may only be discovered at run-time, when the routine is actually called. Furthermore, such errors may be discovered after an arbitrarily long time, when the incorrect call to the 'eat' routine actually occurs during execution.
This loss of static type-safety is inherent in languages that support co-variance, such as Eiffel. The problem can be somewhat ameliorated though the use of type-inference, but there will always be cases where type-inference cannot prove that a certain call is type-safe.
Sather permits the user to break type-safety, but only through the use of a typecase on the arguments. Such case of type un-safety uses are clearly visible in the code and are far from the default in user code.
Another typesafe solution is to parametrize the animal abstraction by the kind of food the animal eats.