In the preceding chapter we discussed collections and components for collections. We assumed that the types of elements of a collection were known in advance. For example, our Stack example only worked for Books and our Queue example was restricted to Vehicles. But what happens if we want to stack or queue elements with other types? Should we make another component for each new element type? That would be a waste of effort, because all components for Stacks (or Queues) would have almost identical structures: the only difference would be the element type information. To avoid that, we will introduce the concept of generic components. Generic components are parameterized components.
In this chapter we will describe the declaration and use of generic components.
We will start again with our well-known Stack example. Based on the Stack component of the preceding chapter, we will introduce a generic component applicable to all types of elements (Figure 12-1).
component Stacks ( Stack, Element ); type Stack; Stack (MaxSize = integer) -> Stack; Empty ( Stack ) -> boolean; Full ( Stack ) -> boolean; Push ( Stack, Element) -> nothing; Pull ( Stack ) -> Element; begin Stack (MaxSize) = Stack:[ MaxSize; index:=0; area=array (Element, MaxSize) ];Empty ( stack ) = (stack.index <= 0);Full ( stack ) = (stack.index >= stack.MaxSize);Push ( stack, element ) = [ exception (Full (stack), "Stack Overflow"); stack.index:=stack.index + 1; stack.area[ stack.index]:=element ];Pull ( stack ) = [ exception (Empty (stack), "Stack Underflow"); stack.index:=stack.index - 1; stack.area[stack.index + 1] ];end component Stacks;
Figure 12-1: An example of a generic component for stacks.
This component supports the same operations as the Stacks component of Section 11.2. The only difference is that the stack type and the element type are parameters of the component. This means that this component will work for any type of elements. So, we may use the component for stacking dishes, or for stacking books, or for stacking notes, provided we will tell the component what the type of the elements are.
There is also another distinction we often like to make: that's about the type of the collection. We normally will regard two stacks of dishes as being collections of the same type, but a stack of dishes and a stack of books are two different types of collections. So, the types of the elements are also determining the types of the collections.
As a consequence, our Stacks component has two generic parameters: one for the type of the stack and one for the type of the elements of the stack.
A generic component is invoked by a use directive with corresponding generic arguments. Let us assume that we want to use the generic Stacks component for stacking books. We want to call our stack of books BookStack and the type of element we want to name Book. This is achieved by using the following use directive:
use Stacks (BookStack, Book);
The effect of this invocation is that an instance of the generic Stacks component is obtained for these particular arguments. The resulting component has the following form:
component Stacks; type BookStack; BookStack (MaxSize = integer) -> BookStack; Empty ( BookStack ) -> boolean; Full ( BookStack ) -> boolean; Push ( BookStack, Book) -> nothing; Pull ( BookStack ) -> Book; begin BookStack (MaxSize ) = BookStack:[ MaxSize; index:=0; area=array (Book, MaxSize) ];Empty ( stack ) = (stack.index <= 0);Full ( stack ) = (stack.index >= stack.MaxSize);Push ( stack, element) = [ exception (Full (stack), "Stack Overflow"); stack.index:= stack.index + 1; stack.area [stack.index ]:= element ];Pull ( stack ) = [ exception (Empty (stack), "Stack Underflow"); stack.index:= stack.index - 1; stack.area [stack.index + 1 ] ];end component Stacks;
Figure 12-2: An example of an instantiation of a generic component.
The effect of invoking a generic component will be clear after studying the resulting component. All references to the generic Stack parameter have been replaced by the corresponding BookStack argument and all references to the generic Element parameter have been replaced by the corresponding Book argument.
This instantiated component can now be used as any other, non-generic, component. A session with this instantiated component is somewhat different from the corresponding session of the previous chapter:
type Book = text; S = BookStack (5); S?
The result is:
BookStack:[ MaxSize = 5; index = 0; area = array( , , , ,  ) ]
The main differences are that the constructor function BookStack has to be called to create a new stack descriptor. And, as we see, the type of the stack descriptor is also named BookStack. The rest of the session is the same.
We are calling the Stacks component a generic component, because it represents a group of components. A generic component has always one or more generic parameters. To instantiate a specific component, the generic component must be invoked by a use directive with proper generic arguments. The names of the generic parameters of the component are replaced by the corresponding generic arguments both in the interface-section and in the implementation-section.
The same generic Stacks component can also be used to stack other kinds of entities. Let us assume that you want to stack our well-known Triangles and also want to stack Circles in different stacks. Your wishes could take the following form:
type Numeric = real; use Points, Triangles, Circles;use Stacks ( Stack_of_Triangles, Triangle); use Stacks ( Stack_of_Circles, Circle);ST1 = Stack_of_Triangles (10); ST2 = Stack_of_Triangles (20);SC1 = Stack_of_Circles (5); SC2 = Stack_of_Circles (7);
In this example, after indicating that we want to use Points, Triangles and Circles, we are invoking the Stacks component two times: the first time to indicate that we want to use a specific component dedicated to Stacks of Triangles, the second time for a specific component dedicated to Stacks of Circles.
The two specific Stacks components can now be used to create different kinds of stacks. ST1 and ST2 are the names of two different Stacks both for Triangles; SC1 and SC2 are Stacks dedicated to Circles.
We are now able to use the operations for the different types. Looking back to this exercise, we see that the first use directive is providing us with all the power of the operations on Points, Triangles, and Circles, and in addition it gives us the operations on two different types of stacks. Some examples of these possibilities are given in the following:
P1 = Point ( 3.0, 4.0); P2 = Point (-23.0, 12.0); P3 = Point ( 12.0, -13.0);Push (ST1, Triangle (P1, P2, P3) ); Push (ST1, Triangle (P3, P2, P1) );T2 = Pull( ST1 ); T1 = Pull( ST1 );Push ( SC1, Circle ( P1, 15.0) ); C1 = Pull ( SC1 );
Generic parameters representing collections types and element types are not the only kinds of generic parameters which are required for generic components. There are cases where also specific values must be instantiated.
Let us use as an example the component for Matrices as discussed in Section 11.4, "Matrices". Our example component was restricted to integer values. However, matrices can also defined for real values, complex values, formula's, and so on. That means that we will need generic parameters for the type of a matrix and the type of a matrix element, just as needed for the generic Stacks component. However, there is an additional complication. If we are looking inside the definition rule for Matrix1 * Matrix2, we discover the assignment:
Matrix3. A[i, j]:=0;
This statement assigns the value zero to a matrix element. However, this value 0 must be of the same type as the element type. That means that the value zero depends on the generic parameter for element type. To solve that problem we will also introduce Zero as an additional generic parameter (see Figure 12-3).
component Matrices (Matrix, Element, Zero); type Matrix; Matrix (Rows = integer, Columns = integer) -> Matrix; Matrix + Matrix -> Matrix; Matrix * Matrix -> Matrix; Fill (Matrix, list (list (Element)) ) -> nothing; begin Matrix (Rows, Columns) = [ M = Matrix:[ Rows; Columns; A = array (array(Element), Rows) ]; M.A[ 1..Rows ]:= array (Element, Columns); M ];
Figure 12-3: An example of a generic component for matrices.
We can now use this generic component for different kinds of generic parameters. For example, if we want to use matrices for integer numbers but also matrices for real numbers, we may write the following use directives:
use Matrices (IntegerMatrix, integer, 0); use Matrices (RealMatrix, real, 0.0);
The last use directive creates a specific component for reals of the following form (see Figure 12-4).
Figure 12-4: An example of a instantiated component for matrices.
In this specific component every reference to Matrix of the generic component has been replaced by RealMatrix, Element has been replaced by real and Zero has been replaced by 0.0.
We may now use this specific component as shown in the following session:
MR1 = RealMatrix (3, 4); MR1?
The result is:
RealMatrix:[ Rows = 3; Columns = 4; A = array( array( 0.0, 0.0, 0.0, 0.0), array( 0.0, 0.0, 0.0, 0.0), array( 0.0, 0.0, 0.0, 0.0) ) ]
These examples of generic components may be extended with many others. For example, some generic components may also need generic parameters for unity, representing 1, or 1.0, and other kinds of generic parameters. We will not do that. It is sufficient if the basic mechanisms of generic component instantiation are understood.
Generic program units were introduced in Ada ( ANSI and AJPO (1983)). Meyer (1988) discusses aspects of genericity versus inheritance.
<!-- Start of StatCounter Code for Default Guide -->
var scJsHost = (("https:" == document.location.protocol) ?
"https://secure." : "http://www.");
<noscript><div class="statcounter"><a title="Web Analytics Made Easy -
StatCounter" href="http://statcounter.com/" target="_blank"><img
alt="Web Analytics Made Easy - StatCounter"></a></div></noscript>
<!-- End of StatCounter Code for Default Guide -->