Is a Cow an Animal?

Title Picture

Are Cows Animals?

by David L. Shang

What a silly question! No doubt they are.

But the answer is not that simple in object-oriented programming. There has been a long debate in history and the debate becomes even hotter today -- whether cows are animals. I wouldn't kid you.

Let's try to define an animal in class:

If a cow is an animal, we can specify a cow as a subclass of Animal. We know that a cow is a herbivore. It should be a common sense to restrict the food type of a cow to PlantFood:

We call this redefinition covariance or narrowing, since the type of an argument in a method interface in a subtype is covariantly redefined into a subtype, and the range of the argument is narrowed in a subtype.

We get a trouble. Consider a polymorphic variable:

According to the interface specification of the eat method in the class Animal, we can feed the animal with any food:

What will happen if the variable is a reference to a cow? The cow must choke, because the eat method defined in Cow can accept plant food only.

What's wrong? Let's have a look at the different answers:

My answer is:

Is there anything wrong with our Animal class definition? Yes. The the interface of the eat method

tells us that for any animal "a" and any food "f",

is a valid combination. In fact, this declaration is wrong. An animal can eat only a certain type of food, not all kinds of food, especially when we are planing to divide the animal class into herbivores and carnivores.

Being a subtype by nature, it should do everything that a supertype can. By narrowing, we declare something that a subclass cannot do while the superclass can. In this case, there is usually no problem with subclasses as we might think. It is very likely that our superclass definition is wrong: the superclass makes a promise (e.g. all animals can eat all food) that can not be supported by subclasses (e.g. tiger does not eat grass). Narrowing thus occurs when the superclass statement is wrong or at least imprecise. The fact that something incorrect must be made truer by subclasses introduces the conflict.

When subclasses require narrowing, there is usually an untrue promise in their superclass. Therefore, narrowing is not naturally required. In other words, subclasses do not need narrowing if their superclass is specified properly.

Therefore, in Animal's eat specification, we cannot simply declare food to be of type of AnyFood. The type of food is a variable to the type of its enclosing object, that is, the type of animal. It depends on the concrete subclass of Animal. We should specify the type of food as a dependent type to the type of animal:

This is a specific kind of type dependency: the type of an argument defined in a class depends on the type of the enclosing object. Therefore, the type of food is a type variable depending on the type of the animal. In the herbovore animal class, we are not necessary to narrow the range of the food type to plant food, because the super class Animal already implies the statement:

In a superclass, the exact type of the dependent type is vague. It becomes clearer and clearer as subclasses are derived. I also call this covariance, but not norrowing, because the range of the argument is not narrowed. I define covariance a way to clarify a vague but fixed range of a component in subclasses. For methods to describe the dependency, click here for detail.

Some people view covariance is an exception; so the run-time type check like mulptiple dispatch or type switch statement is inevitably required. It is true that reality has exceptions, no matter how we don't like them. But are all covaraince exceptions? No. Exceptions are only the properties of a few individual objects and subclasses. Exceptions cannot be in majority, otherwise they are not exceptions. If we find that almost all subclasses are exceptions of a superclass whose argument type needs to be redefined, the definition of the superclass must be wrong. Remember that programming is an art to formalize the reality to computer. In formalization, we always try to establish a mathematic model in which all objects are arranged in a certain pattern and behaves according to certain rules. Exceptions should be eliminated as possible as we can.

Think about a few more examples which are commonly used to illustrate covariance. They are not exceptional covariance, but have type dependency. And narrowing is not required. The component type depends on the type of the enclosing object. Type dependency is the standard to judge whether a covariance is exceptional or not. For example, the type of the vehicle that an operator drives depends on the type of the operator; and the type of the operator that an vehicle is assigned to depends on the type of the vehicle; Therefore, when we define an Operator class, its vehicle type should be the type that the operator can drive. It is not proper to say that an operator can drive any vehicle; otherwise you'll get trouble when you assign an luggage-truck driver to an airplain.

As a conclusion, I'd like to provide my golden rules to you: