A generic class can be a mechanism to specify the type dependency between a component type and its enclosing object type. By constrained genericity, a class parameter is an unknown subclass of a given class. It is usually used to specify a component type or a method interface, whose exact type is not clear yet. The concrete type for a class parameter will be determined at class instantiation. Therefore, the component type specified by a class parameter will depend on a concrete instantiation.
A generic class, however, is traditionally considered a feature of static syntax expansion. Since it is not a real class, we cannot establish the subclass relationship between a generic class and its instantiations. No variables can be specified by a generic class, thus run-time polymorphism on a generic class is impossible.
The BETA and the Transframe (a web page will be available to public in January, 1996) languages provide generic classes, which are real classes. The generic mechanism is achieved by virtual bindings. Class parameters are constrained. (In BETA, class parameters are virtual classes) Re-constraining or binding class parameters produces subclasses. Generic classes can be used to specify polymorphic variables.
Let us reconsider the Animal example in terms of Transframe's generic class:
class Animal < type FoodType : AnyFood >
{
func eat (f: FoodType);
};
where FoodType is a class parameter constrained by the class Food. FoodType will be a subclass of Food but we do not know the concrete type at the abstraction level of Animal.
In HerbivoreAnimal, FoodType is re-constrained to PlantFood:
class HerbivoreAnimal is Animal < type FoodType: PlantFood >;
In Cow, we bind FoodType to Grass:
class Cow is HerbivoreAnimal < type FoodType = Grass >;
Once a class parameter is bound, it cannot be rebound in subclasses. For example, any subclass of Cow should not rebind its FoodType. There will be no further covariance, and any polymorphic variables based on subclasses of Cow and Grass will be guaranteed type safe. For example, the code
x: Cow;
f: Grass;
...
x.eat(f)
is type safe.
Before the class parameter is bound, the (generic) class is an abstract class. For a polymorphic variable specified by the abstract class, we do not know the exact type of a class parameter. However, polymorphic variables are acceptable to a static type checking system if we know by static the type dependency relation between these polymorphic variables. Let us consider a polymorphic variable in a free function (a function outside of a class):
func feed
< type AnimalType: Animal >
( animal: AnimalType; food: AnimalType.FoodType)
{
animal.eat(food);
};
The type dependency is clearly specified in the function interface. Though animal is a polymorphic variable specified by an abstract class, the food type is specified as a dependent type. Therefore, the following variable substitutions:
feed (aTiger, aBunchOfGrass)
feed (aCow, aPieceOfMeat)
will be rejected by the static type checking rule because the compiler knows the exact type of actual parameters.
Function feed can be compiled separately and the expression animal.eat(food) is guaranteed type safe, though the compiler does not know the actual type of animal when feed is compiled. Local checkability is ensured.
If the compiler does not know the exact type of actual parameters when feed is called, type correctness can still be ensured by the compiler if the type dependency is specified among the actual parameters. For example, if feed is called within the take method body of the AnimalGroup class:
class AnimalGroup < type MemberType :Animal >
{
members: MemberType[];
proc take (a:MemberType; f:MemberType.FoodType)
{
...
feed (a, f);
...
}
};
type safeness is guaranteed. And again, if take is called from another environment where the type dependency is specified, the call is safe.
In a close world assumption, we assume that the exact type of any variable should be known eventually by the static type checking system. Therefore, a function call chain in a close world will be eventually originated from a global environment where all the variable types are known statically, and the type checking system can always determine whether the variable types conform to the type dependency specification.
Since the interface precisely declares the type dependency, uses of the interface will not be misled to present some inputs in a wrong type. And the type correctness can be locally ensured by separate compilation. No more surprising erros will be reported at link time or run-time.
Type dependency is not always required, however. It depends on the prgrammer's intension. For example, if one wants to define a try_and_eat method in the animal class:
class Animal < type FoodType : AnyFood >
{
func eat (f: FoodType);
func try_then_eat (f: AnyFood);
};
It is equally fine. The herbivore subclass should not restrict the try_then_eat interface to PlantFood only, because its superclass regulates that for any animal "a" and any food "f", "a.try_then_eat(f) is valid. A subclass cannot voilate the regulation.
Runtime type check is inevitably necessary when we try to attach an object of a superclass to a variable in a subclass. We call this attachment reverse assignment. If we try to call the method eat in try_then_eat:
func try_then_eat (f: AnyFood);
{
eat(f);
}
This is a reverse assignment, becuase the actual parameter is in AnyFood, a superclass of this animal's FoodType. By Transframe, "eat(f)" is invalid unless a type assurance statement is used:
func try_then_eat (f: AnyFood);
{
when (typeof(f) is FoodType) eat(f);
}
By using type assurance statement for reverse assignment, a compiled program in Transframe will be run-time error free.