En este capítulo se describe el mecanismo genérico (generic) que permite parametrizar subprogramas y paquetes con tipos y subprogramas así como valores y objetos.
Declaraciones e Instanciaciones
Uno de los problemas con un lenguaje tipificado, como Ada, es que todos los tipos deben ser determinados en tiempo de compilación. Esto significa naturalmente que no podemos pasar tipos como parámetros en tiempo de ejecución. Pero con frecuencia llegamos a la situación en que la lógica de una pieza de programa es independiente de los tipos involucrados y por lo tanto pareciese ser innecesario el repetirla para todos los diferentes tipos para los cuales pudiésemos desear aplicarla. Un ejemplo simple está propuesto por el procedimiento SWAP.
procedure SWAP(X, Y: in out Real) is
T: Real;
begin end;
T:=X; X:=Y; Y:=T;
Es claro que la lógica es independiente del tipo de los valores que están siendo intercambiados. Si también quisiésemos intercambiar enteros o booleanos podríamos escribir otros procedimientos, pero esto sería tedioso. El mecanismo genérico nos permite superar esto. Podemos declarar:
generic
type ITEM is private;
procedure EXCHANGE(X,Y: in out ITEM);
procedure EXCHANGE(X,Y: in out ITEM) is
T:ITEM;
begin end;
T:=X; X:=Y; Y:=T,
El subprograma EXCHANGE es un subprograma genérico y actúa como una plantilla (template). La especificación del subprograma es precedida por la parte formal genérica consistente de la palabra reservada generic seguida de una lista (posiblemente vacía) de parámetros genéricos formales. El cuerpo del subprograma está escrito igual que siempre pero hay que notar que, en caso de un subprograma genérico, debemos escribir la especificación y el cuerpo separadamente.
El procedimiento genérico no puede ser llamado directamente, pero a partir de él podemos crear un
procedimiento efectivo mediante el mecanismo conocido como instanciación genérica. Por ejemplo, podríamos escribir:
procedure SWAP is new EXCHANGE(REAL);
En esta declaración se indica que SWAP debe ser obtenido de la plantilla descrita por EXCHANGE. Los parámetros genéricos reales son proporcionados en una lista de parámetros en la forma usual. El parámetro real (en el ejemplo es el tipo REAL) corresponde al parámetro formal ITEM.
De modo que ahora hemos creado el procedimiento SWAP actuando sobre el tipo REAL y podemos por consiguiente llamarlo en la forma usual. Podemos crear nuevas instanciaciones como:
procedure SWAP is new EXCHANGE(INTEGER);
procedure SWAP is new EXCHANGE(DATE);
y muchas más. Nótese que estamos creando nuevas sobrecargas de SWAP, las cuales pueden ser distinguidas por sus tipos de parámetros del mismo modo que si las hubiésemos escrito en detalle.
Superficialmente, puede parecer que el mecanismo genérico es simplemente una substitución de texto y en efecto, en este sencillo ejemplo, el comportamiento es el mismo. Sin embargo la diferencia importante se relaciona con el significado de los identificadores utilizados en el cuerpo genérico, y que no son ni parámetros ni objetos locales. Tales identificadores no locales poseen significados de acuerdo a donde fue declarado el cuerpo genérico y no donde éste es instanciado. Si se usara la simple substitución de texto, los identificadores no locales podrían, por supuesto, tomar su significado en el punto de instanciación y esto podría producir resultados distintos de los esperados.
Así como podemos escribir subprogramas genéricos también podemos tener paquetes genéricos. Un ejemplo simple de esto es entregado por el paquete STACK. El problema con ese paquete, es que sólo trabaja sobre tipos INTEGER aunque, por supuesto, la misma lógica se aplica sin distinción del tipo de los
valores manipulados. También podemos aprovechar la oportunidad para hacer de MAX un parámetro de la misma forma, de tal modo que no estamos atados a un límite arbitrario de 100. Escribimos:
generic
MAX:POSITIVE;
type ITEM is private;
package STACK is
procedure PUSH(X: ITEM);
function POP return ITEM;
end STACK;
package body STACK is
S: array (1..MAX) of ITEM; TOP: INTEGER range 0..MAX;
— el resto como antes, pero donde aparecía INTEGER
— ahora aparece ITEM
end STACK;
Ahora podemos crear y usar una pila de un tipo y un tamaño particular mediante la instanciación del paquete genérico de la siguiente forma:
declare
begin
end;
package MY_STACK is new STACK(100, REAL);
use MY_STACK;
….
PUSH(X);
…. Y:=POP;
….
El paquete MY_STACK, que es el resultado de la instanciación, se comporta como un paquete escrito directamente de la forma normal. La cláusula use nos permite referirnos directamente tanto a PUSH como a POP. Si hiciéramos una instanciación posterior
package ANOTHER_STACK is new STACK(50, INTEGER);
use ANOTHER_STACK;
entonces PUSH y POP son sobrecargas que pueden ser distinguidas por el tipo entregado por el contexto. Por supuesto, si ANOTHER_STACK también fuera declarado con el parámetro genérico real REAL, entonces deberíamos usar notación punto para distinguir las instancias PUSH y POP a pesar de las cláusulas use.
Tanto las unidades genéricas y las instanciaciones pueden ser unidades de biblioteca. De este modo,
habiendo puesto el paquete genérico STACK en la biblioteca de programas se podría realizar una instanciación y compilarla separadamente..
with STACK;
package BOOLEAN_STACK is new STACK(200, BOOLEAN);
Si agregáramos una excepción de nombre ERROR al paquete, de tal modo que la declaración del paquete genérico fuese:
generic
MAX: POSITIVE;
type ITEM is private;
package STACK is
ERROR: exception; procedure PUSH(X: ITEM); function POP return ITEM;
end STACK;
entonces cada instanciación debería dar origen a una excepción distinta y debido a que las excepciones no pueden ser sobrecargadas naturalmente tendríamos que usar la notación punto para distinguirlos.
Podríamos, por supuesto, hacer la excepción ERROR común a todas las instanciaciones definiéndola como global para todo el paquete genérico. Esta y el paquete genérico podrían quizá ser declarados dentro de otro paquete.
package ALL_STACK is
ERROR: exception; generic
MAX: POSITIVE;
type ITEM is private; package STACK is
procedure PUSH(X: ITEM);
function POP return ITEM;
end STACK;
end ALL_STACKS;
package body ALL_STACK is package body STACK is
….
end STACK;
end ALL_STACK;
Esto ilustra la ligación de los identificadores globales con las unidades genéricas. El significado de ERROR queda determinado en el lugar de la declaración genérica, independiente del significado que pudiese tener en el punto de instanciación.
Los ejemplos anteriores han ilustrado parámetros formales, los cuales eran tipos y también enteros. En efecto, los parámetros formales genéricos pueden ser cualquiera de los parámetros aplicables a subprogramas; pero también pueden ser tipos y subprogramas.
En el caso de los parámetros ya conocidos que también se aplican a subprogramas, estos pueden ser de modo in o in out, pero no out. Como con los subprogramas, in es tomado por omisión (como MAX en el ejemplo anterior).
Un parámetro genérico in actúa como una constante cuyo valor es entregado por el parámetro real correspondiente. Se permiten expresiones por omisión como en los parámetros de subprogramas; tal expresión es evaluada durante la instanciación si no se suministran los parámetros reales del mismo modo que en los subprogramas.
Un parámetro in out, actúa como una variable que renombra el parámetro real correspondiente. El parámetro real debe por tanto ser el nombre de una variable y su identificación ocurre en el punto de instanciación.
Nuestro último ejemplo en esta sección ilustra el anidamiento de genéricos. El siguiente procedimiento genérico realiza un intercambio cíclico de tres valores y está escrito en términos del procedimiento genérico EXCHANGE.
generic
type THING is private;
procedure CAB(A, B, C: in out THING);
procedure CAB(A, B, C: in out THING) is
procedure SWAP is new EXCHANGE(ITEM => THING);
begin
SWAP(A, B);
SWAP(A, C);
end CAB;
Aunque el anidamiento está permitido, este no debe ser recursivo. Ejercicio.
- Escriba la declaración de un paquete genérico que implemente el tipo abstracto de datos PILA (es decir, que se puedan definir variables de tipo PILA) de forma tal que se pueda variar el tamaño de la pila y el tipo que se pueda almacenar. Si se instanciasen dos paquetes:PILA_REALES y PILA_ENTEROS. ¿Qué problema habría al declarar, por ejemplo X:PILA? ¿Cómo se solucionaría el problema? ¿Habría problemas al usar directamente POP y PUSH?
- Escriba un paquete genérico que permita definir variables de tipo ARREGLO a las cuales se le puede indicar el largo y el tipo de sus elementos. Sobre objetos tipo ARREGLO se pueden realizar las siguientes acciones: colocar (colocar un valor en una cierta posición), ordenar, invertir, primero (entrega el primer elemento) y último (entrega el último elemento).
Subprogramas como parámetros
Los parámetros genéricos también pueden ser subprogramas. En algunos lenguajes, como Algol y Pascal, los parámetros de subprogramas pueden a su vez ser subprogramas. Esta facilidad es útil para
aplicaciones matemáticas como la integración. En Ada, los subprogramas sólo pueden ser parámetros de unidades genéricas de modo que para estas aplicaciones se usa el mecanismo genérico.
Podríamos tener una función genérica
generic
with function F(X: REAL) return REAL;
function INTEGRATE (A, B: REAL) return REAL;
la cual evalúa
b
? f ( x)dx
a
para integrar una función en particular debemos instanciar INTEGRATE con nuestra función como un parámetro genérico real. Así, supongamos que necesitamos integrar la función
et sin t entre los límites 0 y P
entonces escribiríamos
function G(T: REAL) return REAL is begin
end;
return EXP(T)*SIN(T);
function INTEGRATE_G is new INTEGRATE(G); y nuestro problema queda resuelto mediante la expresión
INTEGRATE_G(0.0, P)
Nótese que un parámetro subprograma formal es como una declaración normal de subprograma precedida por with. (La palabra with al inicio es necesaria para evitar una ambigüedad sintáctica y no posee otro propósito.) La correspondencia entre subprogramas formales y reales es tal que el subprograma formal actúa sólo como un nuevo nombre para el subprograma real.
Ejercicio.
Dada la función
generic
with function F(X: REAL) return REAL;
function SOLVE return REAL;
que encuentra una raíz de la ecuación f(x) = 0, muestre como encontraría la raíz de la ecuación
ex + x = 7
¿Cómo haría que el tipo del parámetro de la función F también pudiese definirse durante la instanciación?
0 comentarios