Programación genérica en Ada ── escribir contratos con tipos y lograr la reutilización sin costo en tiempo de ejecución
· Actualizado el: · Go Komura · Ada, ProgrammingLanguage, Generics, TypeSystem, StaticTyping, ContractModel, ZeroCostAbstraction, GNAT, Alire, Alta fiabilidad, Reutilización de código
1. Introducción ── no «aceptar cualquier cosa», sino «qué se promete»
Al intentar escribir código reutilizable en un lenguaje de tipado estático, enseguida se topa con el mismo problema. Quiere usar para cadenas la misma pila que escribió para enteros. Quiere aplicar el mismo procesamiento estadístico a un arreglo de punto flotante. Quiere reutilizar la lógica de ordenamiento ascendente también para el descendente. Sin embargo, si copia el mismo código para cada tipo, se producen olvidos al corregirlo. Y si, a la inversa, diseña algo que acepta cualquier cosa mediante void* o conversiones de tipo, la seguridad de tipos se rompe.
La respuesta de Ada son los genéricos (generic units).
Los genéricos de Ada no son una simple sustitución de texto. Reciben tipos, valores, subprogramas e incluso paquetes enteros como parámetros formales, y se comprueban estáticamente en el momento de la instanciación. Es decir, en lugar de averiguar en tiempo de ejecución si «este tipo realmente sirve», es un mecanismo que determina en tiempo de compilación si «este componente cumple este contrato».
flowchart LR
A[Proceso que se quiere reutilizar] --> B{¿Cómo reutilizarlo?}
B --> C[Copiar y pegar]
B --> D[void* / Object / conversión de tipo]
B --> E[Genérico de Ada]
C --> C1[Es fácil olvidar alguna corrección]
D --> D1[Es fácil que aparezcan errores en tiempo de ejecución o pérdida de seguridad de tipos]
E --> E1[Seguro en cuanto a tipos]
E --> E2[Comprobación en tiempo de compilación]
E --> E3[Sin despacho adicional en tiempo de ejecución]
En este artículo se organiza la programación genérica de Ada según el siguiente recorrido.
- Subprogramas genéricos
- Paquetes genéricos
- Parámetros de tipo, de valor y de subprograma
- Categorías de tipo como
private,range <>,digits <> - Ejemplos de implementación: ordenamiento, pila, procesamiento estadístico,
Count_Ify almacén clave-valor - Genéricos de orden superior mediante el parámetro formal de paquete
- El contract model de Ada y criterios de diseño para la práctica
1.1 Público objetivo y qué se lleva de este artículo
Este artículo está pensado para lectores como los siguientes.
- Quienes tienen experiencia con las plantillas de C++, los genéricos de C# o Java, o los genéricos de Rust.
- Quienes no conocen a fondo la sintaxis de Ada, pero les interesa la idea de diseño de «escribir contratos mediante tipos».
- Quienes están estudiando criterios de diseño para componentes reutilizables en mantenimiento a largo plazo, sistemas embebidos o software de alta fiabilidad.
En cuanto a la sintaxis de Ada, basta con conocer los paquetes (la separación entre la especificación .ads y el cuerpo .adb) y los modos de parámetro in / out / in out para seguir la lectura. Si estos dos puntos le resultan inseguros, conviene leer antes «El atractivo del lenguaje Ada» para entender más rápido.
Más que la sintaxis de Ada en sí, lo que se lleva de aquí es una disciplina de diseño: expresar como tipos, antes que como implementación, lo que exige un componente reutilizable. Esta idea se puede aplicar directamente también al decidir hasta qué punto escribir restricciones de interfaz en C# o concepts en C++20.
1.2 Guía de lectura ── no hace falta leerlo todo
Este artículo tiene 20 capítulos. No hace falta leerlo de corrido: según el objetivo, se puede leer de forma selectiva como se indica a continuación.
| Objetivo | Capítulos a leer |
|---|---|
| Entender solo la idea en poco tiempo | Cap. 4 (modelo básico) → cap. 6 (subprograma genérico mínimo) → cap. 13 (contract model) |
| Quiero escribir mis propios componentes | Cap. 4 → cap. 6 → cap. 7 (paquete genérico) → cap. 8 (inyección de comportamiento) → cap. 9 (categorías de tipo) |
| Solo quiero criterios para decisiones de diseño | Cap. 13 → cap. 14 (qué conviene hacer genérico) → cap. 15 (errores frecuentes) → cap. 17 (lista de comprobación) |
| Quiero poner manos a la obra | Preparar el entorno en el cap. 3 y partir de los ejemplos completos de los cap. 6 y 7 |
La ruta más corta son los tres capítulos 4, 6 y 13. Con estos tres capítulos quedan cubiertos los elementos centrales: los parámetros formales, la instanciación y el contract model. Los capítulos 5 y 9 son listados de parámetros formales, así que puede consultarlos como un diccionario cuando los necesite.
Este tema continúa la serie de este blog: «El atractivo del lenguaje Ada», «Introducción a la verificación formal con SPARK», «Concurrencia segura» y «Sistemas de tiempo real». Profundiza en la idea de Ada de «expresar el diseño mediante tipos» desde el ángulo de los genéricos.
2. Mapa de este artículo
Primero se capta la visión de conjunto con un diagrama. Entender los genéricos de Ada solo como «una función que toma un tipo como argumento» es una visión bastante estrecha. En realidad, según la unidad que se quiera reutilizar, se combinan subprogramas, paquetes, parámetros de subprograma, parámetros de valor y parámetros formales de paquete.
mindmap
root((Genéricos de Ada))
Subprograma genérico
Swap
Count_If
Sort
Paquete genérico
Stack
Statistics
KV Store
Parámetros formales
Tipo
private
limited private
range box
mod box
digits box
delta box
discrete box
Objeto
Max_Size
Threshold
Subprograma
función Less
función Equals
Predicate
Paquete
with package P is new Generic
Ideas de diseño
Contract Model
Comprobación estática
Abstracción sin costo
Separación de especificación y cuerpo
En el diagrama, box se refiere al <> de Ada (el box compound delimiter). Como Mermaid no puede representar <> directamente, en el diagrama se escribe únicamente box.
La forma de leer este artículo es sencilla. En la primera mitad se sigue la sintaxis, y en la segunda mitad se abordan las decisiones de diseño. Si es la primera vez que lee Ada, no intente memorizar los detalles de la sintaxis desde el principio; preste atención a «qué se convierte en parámetro formal» y a «qué operaciones se permiten sobre ese parámetro formal».
2.1 Mini diccionario de términos
A continuación se relacionan por adelantado, en español e inglés, los términos que aparecerán una y otra vez en el artículo. En este artículo, generic se traduce como «genérico», y se usan también las expresiones «subprograma genérico» y «paquete genérico»; el significado es el mismo que el de generic unit.
| Término en este artículo | Inglés | Significado |
|---|---|---|
| unidad genérica | generic unit | Declaración que empieza con generic. Denominación conjunta del subprograma genérico y el paquete genérico |
| parámetro formal | generic formal parameter | El argumento del lado receptor, escrito entre generic y el cuerpo de la declaración. Puede ser un tipo, un valor, un subprograma o un paquete |
| formal part | generic formal part | La parte donde se enumeran los parámetros formales. Puede entenderse como «el lugar donde se escribe el contrato» |
| parámetro real | generic actual parameter | El tipo, valor, subprograma o paquete que se pasa realmente en el momento de la instanciación |
| instanciación | instantiation | Crear, mediante new, un subprograma o paquete normal a partir de una unidad genérica |
| box | <> |
Nombre del símbolo <> en Ada. Expresa, como en range <>, que «el tipo concreto se decide en el momento de la instanciación» |
| contract model | contract model | El método de Ada por el que el cuerpo se escribe únicamente dentro de lo prometido en los parámetros formales, y se comprueba el tipo del cuerpo por sí solo (capítulo 13) |
En el texto se escribe range <>, digits <>, siguiendo la notación de Ada, pero en los diagramas se escribe únicamente range box, digits box. Esto se debe a que Mermaid no puede representar directamente etiquetas que contienen <>, y el significado es el mismo que en el texto. Como muestra la tabla anterior, el propio Ada llama box a <>, así que la notación de los diagramas tampoco se aparta de la terminología de Ada.
3. Entorno de ejecución y compilación
El código de este artículo asume GNAT 15.x o posterior. GNAT es el compilador de Ada más representativo y se puede instalar desde Alire. Alire es el gestor de paquetes de Ada / SPARK, y también sirve para gestionar la cadena de herramientas y para compilar.
gnat --version
# GNAT 15.2.1
Instale GNAT desde Alire (el gestor de paquetes de Ada) con alr install gnat_native gprbuild, y añádalo al PATH.
Se supone que las muestras tratadas en este artículo se ubican dentro del repositorio de la siguiente manera.
flowchart TB
R[ada-generic-programming/] --> S[src/]
S --> N[snippets/]
N --> A[01_swap.ada]
N --> B[02_stack.ada]
N --> C[03_sort.ada]
N --> D[04_statistics.ada]
N --> E[05_filter.ada]
N --> F[06_kv_store.ada]
R --> README[README.md]
Las muestras que reúnen varias unidades de compilación en un solo archivo se dividen con gnatchop y después se compilan con gnatmake.
mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo
-gnata es la opción que habilita las aserciones. No es imprescindible para usar los genéricos en sí, pero en las muestras de aprendizaje facilita comprobar los contratos y las condiciones límite.
sequenceDiagram
participant Dev as Desarrollador/a
participant Chop as gnatchop
participant Build as gnatmake
participant Exe as Ejecutable
Dev->>Chop: Pasar un único archivo .ada
Chop-->>Dev: Dividir en .ads / .adb / main
Dev->>Build: gnatmake -gnata main
Build-->>Dev: Ejecutar hasta el enlace, incluido el binding
Dev->>Exe: ./main
Exe-->>Dev: Resultado de la ejecución
4. Modelo básico de los genéricos de Ada
Los genéricos de Ada se entienden mejor si se piensan, a grandes rasgos, en estos tres pasos.
- Escribir la unidad genérica
- Escribir los parámetros formales en la parte
generic - Instanciar con
newen el lado que la usa
flowchart LR
G[declaración generic] --> F[parámetros formales]
F --> B[cuerpo genérico]
B --> I[instanciación con new]
I --> U[uso como subprograma o paquete normal]
F --> F1[Tipo]
F --> F2[Valor]
F --> F3[Subprograma]
F --> F4[Paquete]
Por ejemplo, al convertir en genérico el procesamiento que intercambia dos valores, se puede dejar el tipo como único parámetro formal.
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
En este punto, Generic_Swap todavía no se puede llamar. Es «una plantilla de intercambio utilizable para cualquier tipo Element». Solo al proporcionar un tipo concreto se convierte en un procedimiento normal.
procedure Swap_Integer is new Generic_Swap (Integer);
En forma de diagrama, la relación es la siguiente.
flowchart TB
Template[Generic_Swap<br/>type Element is private] -->|se pasa Integer| SwapInt[Swap_Integer]
Template -->|se pasa Character| SwapChar[Swap_Character]
Template -->|se pasa My_Record| SwapRecord[Swap_My_Record]
SwapInt --> ICall[Intercambia variables Integer]
SwapChar --> CCall[Intercambia variables Character]
SwapRecord --> RCall[Intercambia variables My_Record]
Lo importante es que el cuerpo de la plantilla está escrito únicamente con las operaciones que se pueden usar sobre Element. Cuando se declara type Element is private;, se pueden usar operaciones básicas como la asignación o la comparación de igualdad, pero no comparaciones de orden ni operaciones aritméticas. Es decir, la propia declaración del genérico expresa «qué puede dar por hecho este componente».
5. Tipos de parámetros formales ── el vocabulario de los genéricos de Ada
Lo que se puede recibir en un genérico de Ada no se limita a los tipos. Este es un punto muy distinto en comparación con los genéricos habituales de C# o Java.
flowchart TB
P[generic formal parameters] --> T[Parámetro de tipo]
P --> O[Parámetro de objeto / valor]
P --> S[Parámetro de subprograma]
P --> PKG[Parámetro de paquete]
T --> T1[type Element is private]
T --> T2[type Index is box]
T --> T3[type Real is digits box]
O --> O1[Max_Size : Positive]
O --> O2[Default_Value : Element]
S --> S1[with function Less...]
S --> S2[with procedure Put ...]
PKG --> P1[with package P is new ...]
En el diagrama, box se refiere al <> de Ada (el box compound delimiter). Como Mermaid no puede representar <> directamente, en el diagrama se escribe únicamente box.
Los parámetros formales representativos, en forma de tabla, son los siguientes.
| Tipo | Ejemplo | Significado |
|---|---|---|
| Parámetro de tipo | type Element is private; |
Forma básica que recibe cualquier tipo definite no limited |
| Parámetro de tipo limited | type Element is limited private; |
También recibe tipos que no se pueden copiar |
| Tipo discreto | type Index is (<>); |
Tipos utilizables como índice de arreglo, como los enteros o los enumerados |
| Tipo entero con signo | type Count is range <>; |
Permite dar por hechas operaciones enteras como +, - y comparaciones de orden |
| Tipo entero modular | type Word is mod <>; |
Permite trabajar con operaciones a nivel de bits o enteros de tipo módulo |
| Tipo de punto flotante | type Real is digits <>; |
Float, Long_Float, tipos de punto flotante definidos por el usuario, etc. |
| Tipo de punto fijo | type Money is delta <>; |
Permite trabajar con aritmética de punto fijo |
| Parámetro de valor | Max_Size : Positive; |
Fija por instancia valores como un tamaño o un umbral |
| Subprograma | with function Predicate (...) return Boolean; |
Inyecta comportamiento, como una función de comparación o un predicado |
| Paquete | with package P is new Some_Generic (<>); |
Recibe como componente un paquete genérico ya instanciado |
Gracias a este vocabulario, en Ada resulta natural escribir «solo recibo los tipos que pueden hacer esta operación», en lugar de «recibo cualquier cosa, pero dentro hago algo peligroso».
6. Subprograma genérico ── entender la estructura mínima con Generic_Swap
Como primer ejemplo, veamos Generic_Swap, que intercambia dos variables de cualquier tipo.
6.1 Especificación
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
La parte que sigue a generic es el parámetro formal. Aquí se recibe un tipo llamado Element. is private significa que, visto desde el cuerpo del genérico, no se conoce la representación interna de ese tipo.
De esta declaración se desprenden estas dos cosas.
Generic_Swapse puede usar con cualquier tipoElement- El cuerpo no depende de la estructura interna de
Elementni de comparaciones de orden
6.2 Cuerpo
procedure Generic_Swap (A, B : in out Element) is
Temp : constant Element := A;
begin
A := B;
B := Temp;
end Generic_Swap;
En este cuerpo solo se usa la asignación sobre Element. No se usa ni A < B ni A + B. Por lo tanto, se puede usar de forma natural con cualquier tipo que admita asignación: Integer, Character, tipos registro, tipos enumerados, etc.
flowchart LR
subgraph Before[Antes de la llamada]
A1[A = 10]
B1[B = 20]
end
A1 --> T[Temp = A]
B1 --> A2[A = B]
T --> B2[B = Temp]
subgraph After[Después de la llamada]
A2[A = 20]
B2[B = 10]
end
6.3 Instanciación
En el lado que lo usa, se emplea new.
procedure Swap_Int is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);
Con esto, Swap_Int y Swap_Char se pueden llamar como procedimientos normales.
with Ada.Text_IO; use Ada.Text_IO;
procedure Swap_Demo is
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
procedure Generic_Swap (A, B : in out Element) is
Temp : constant Element := A;
begin
A := B;
B := Temp;
end Generic_Swap;
procedure Swap_Int is new Generic_Swap (Integer);
X : Integer := 10;
Y : Integer := 20;
begin
Put_Line ("Before: X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
Swap_Int (X, Y);
Put_Line ("After : X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
end Swap_Demo;
El resultado de la ejecución es el siguiente.
Before: X= 10, Y= 20
After : X= 20, Y= 10
Aquí no se puede pasar una variable Float a Swap_Int (X, Y);, porque Swap_Int es un procedimiento normal instanciado específicamente para Integer. Resulta más fácil de entender si se piensa que un genérico no es «un hueco donde cabe cualquier cosa», sino «un mecanismo que crea, para cada tipo, un elemento concreto y seguro».
7. Paquete genérico ── parametrizar el tipo y el valor
Cuando se quiere reutilizar, en conjunto, varias operaciones y un estado interno, en lugar de un solo subprograma, se usa un paquete genérico. El ejemplo representativo es la pila.
En la pila, la lógica básica es la misma; solo cambian el tipo de elemento y el tamaño máximo.
flowchart TB
G[Generic_Stack] --> P1[Element_Type]
G --> P2[Max_Size]
G --> Ops[Push / Pop / Size / Is_Empty / Is_Full]
G --> I1[Int_Stack<br/>Element=Integer<br/>Max_Size=5]
G --> I2[Float_Stack<br/>Element=Float<br/>Max_Size=3]
G --> I3[String_Stack<br/>Element=Unbounded_String<br/>Max_Size=20]
7.1 Especificación
generic
type Element_Type is private;
Max_Size : Positive;
package Generic_Stack is
procedure Push (Item : Element_Type);
function Pop return Element_Type;
function Is_Empty return Boolean;
function Is_Full return Boolean;
function Size return Natural;
Stack_Overflow : exception;
Stack_Underflow : exception;
end Generic_Stack;
Aquí se usan dos tipos de parámetros formales.
Element_Typees un parámetro de tipoMax_Sizees un parámetro de valor
Como Max_Size es Positive, no se puede instanciar con un tamaño de 0 o menor. De esta manera, también se pueden imponer restricciones de tipo sobre los parámetros de valor.
7.2 Cuerpo
package body Generic_Stack is
subtype Index_Type is Positive range 1 .. Max_Size;
type Storage_Type is array (Index_Type) of Element_Type;
Data : Storage_Type;
Top : Natural := 0;
procedure Push (Item : Element_Type) is
begin
if Top = Max_Size then
raise Stack_Overflow;
end if;
Top := Top + 1;
Data (Top) := Item;
end Push;
function Pop return Element_Type is
Result : Element_Type;
begin
if Top = 0 then
raise Stack_Underflow;
end if;
Result := Data (Top);
Top := Top - 1;
return Result;
end Pop;
function Is_Empty return Boolean is
begin
return Top = 0;
end Is_Empty;
function Is_Full return Boolean is
begin
return Top = Max_Size;
end Is_Full;
function Size return Natural is
begin
return Top;
end Size;
end Generic_Stack;
Lo importante en este cuerpo del paquete es que Data y Top se crean por separado en cada instancia.
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
Estos dos se crean a partir de la misma plantilla, pero no comparten estado interno.
flowchart LR
Template[Generic_Stack] --> IntStack[Int_Stack]
Template --> FloatStack[Float_Stack]
subgraph I[Estado de Int_Stack]
ITop[Top]
IData[Data : arreglo de Integer]
end
subgraph F[Estado de Float_Stack]
FTop[Top]
FData[Data : arreglo de Float]
end
IntStack --> I
FloatStack --> F
7.3 Transiciones de estado de la pila
La pila resulta fácil de entender si se ve como una máquina de estados.
stateDiagram-v2
[*] --> Empty
Empty --> NonEmpty: Push
NonEmpty --> NonEmpty: Push / Pop
NonEmpty --> Empty: Pop extrae el último elemento
NonEmpty --> Full: Push alcanza Max_Size
Full --> NonEmpty: Pop
Full --> Overflow: Push
Empty --> Underflow: Pop
7.4 Ejemplo de uso
with Ada.Text_IO; use Ada.Text_IO;
procedure Stack_Demo is
generic
type Element_Type is private;
Max_Size : Positive;
package Generic_Stack is
procedure Push (Item : Element_Type);
function Pop return Element_Type;
function Is_Empty return Boolean;
function Is_Full return Boolean;
function Size return Natural;
Stack_Overflow : exception;
Stack_Underflow : exception;
end Generic_Stack;
package body Generic_Stack is
subtype Index_Type is Positive range 1 .. Max_Size;
type Storage_Type is array (Index_Type) of Element_Type;
Data : Storage_Type;
Top : Natural := 0;
procedure Push (Item : Element_Type) is
begin
if Top = Max_Size then
raise Stack_Overflow;
end if;
Top := Top + 1;
Data (Top) := Item;
end Push;
function Pop return Element_Type is
Result : Element_Type;
begin
if Top = 0 then
raise Stack_Underflow;
end if;
Result := Data (Top);
Top := Top - 1;
return Result;
end Pop;
function Is_Empty return Boolean is (Top = 0);
function Is_Full return Boolean is (Top = Max_Size);
function Size return Natural is (Top);
end Generic_Stack;
package Int_Stack is new Generic_Stack (Integer, 5);
begin
Int_Stack.Push (10);
Int_Stack.Push (20);
Int_Stack.Push (30);
Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
end Stack_Demo;
Se compila y ejecuta con el mismo procedimiento que en el capítulo 3.
gnatchop ../src/snippets/02_stack.ada
gnatmake -gnata stack_demo
./stack_demo
Size= 3
Pop = 30
Pop = 20
Size= 1
El espacio que aparece a la derecha del = se debe a que la especificación de 'Image para tipos enteros coloca un carácter de espacio antes de los valores no negativos. Como se llamó a Push tres veces y a Pop dos veces, el Size final es 1. Además, si se llama a Push cuando la pila ya está llena (en este ejemplo, con los 5 elementos de Max_Size), se lanza Int_Stack.Stack_Overflow, y si se llama a Pop con la pila vacía, se lanza Int_Stack.Stack_Underflow.
En la práctica, los paquetes genéricos funcionan bien en casos como «contenedores pequeños», «búferes de tamaño fijo», «búferes circulares», «colas de registro» o «capas de abstracción de hardware». En particular, en Ada, un diseño que fija de forma estática el tamaño como parámetro de tipo o de valor, en lugar de dejarlo variar en tiempo de ejecución, funciona bien junto con sistemas de alta fiabilidad.
8. Parámetro formal de subprograma ── inyectar comportamiento
Incluso recibiendo solo el tipo, hay cosas que todavía no se pueden expresar. Por ejemplo, en el ordenamiento no basta con el tipo de elemento; también se necesita la lógica de comparación de «cuál va primero».
En Ada, esta función de comparación se puede convertir en un parámetro formal del genérico.
flowchart LR
A[Generic_Insertion_Sort] --> T[Item_Type]
A --> I[Index]
A --> ARR[Item_Array]
A --> CMP[Función de comparación]
CMP --> ASC[Usar la comparación estándar]
CMP --> DESC[Pasar Greater para orden descendente]
CMP --> CUSTOM[Pasar un orden propio]
8.1 Especificación
generic
type Item_Type is private;
type Index is (<>);
type Item_Array is array (Index range <>) of Item_Type;
with function "<" (Left, Right : Item_Type) return Boolean is <>;
procedure Generic_Insertion_Sort (Items : in out Item_Array);
Aquí hay cuatro parámetros formales.
Item_Type: el tipo de los elementos del arregloIndex: el tipo del índice del arregloItem_Array: el tipo de arreglo real"<": la función de comparación
type Index is (<>); recibe un tipo discreto. No solo tipos enteros, sino también tipos enumerados. Lo característico de Ada es que, además de usar Positive como índice de arreglo, se puede usar un tipo enumerado como Day.
El is <> de with function "<" ... is <>; significa que, si se omite el parámetro real, se usa el operador estándar visible o una función compatible. Es decir, con un tipo que ya tiene <, como Integer, se puede usar sin necesidad de indicar explícitamente la función de comparación.
8.2 Cuerpo
procedure Generic_Insertion_Sort (Items : in out Item_Array) is
J : Index;
Key : Item_Type;
begin
if Items'Length <= 1 then
return;
end if;
for I in Index'Succ (Items'First) .. Items'Last loop
Key := Items (I);
J := I;
while J > Items'First and then Key < Items (Index'Pred (J)) loop
Items (J) := Items (Index'Pred (J));
J := Index'Pred (J);
end loop;
Items (J) := Key;
end loop;
end Generic_Insertion_Sort;
El ordenamiento por inserción no es adecuado para arreglos grandes, pero sí resulta adecuado para explicar los genéricos, porque, con solo cambiar la función de comparación, se puede usar la misma estructura de bucle tanto para orden ascendente como descendente.
flowchart TB
Start[Arreglo sin ordenar] --> Pick[Extraer Key de izquierda a derecha]
Pick --> Compare{Key va antes que el elemento anterior?}
Compare -->|Yes| Shift[Desplazar el elemento anterior a la derecha]
Shift --> Compare
Compare -->|No| Insert[Insertar Key]
Insert --> Done{Se procesó hasta el final?}
Done -->|No| Pick
Done -->|Yes| End[Arreglo ordenado]
8.3 Crear orden ascendente y descendente a partir del mismo cuerpo
type Int_Array is array (Positive range <>) of Integer;
procedure Sort_Asc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array);
function Greater (Left, Right : Integer) return Boolean is
(Left > Right);
procedure Sort_Desc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array,
"<" => Greater);
Sort_Asc usa el < estándar. Sort_Desc, en cambio, sustituye la función de comparación mediante "<" => Greater.
flowchart LR
Data[99, 3, 47, 12] --> A[Sort_Asc<br/>comparación estándar]
Data --> D[Sort_Desc<br/>pasa Greater como función de comparación]
A --> AO[3, 12, 47, 99]
D --> DO[99, 47, 12, 3]
Este mecanismo se parece al diseño de C++ que pasa un objeto de función de comparación como argumento de plantilla, o al de Rust que exige un orden mediante trait bounds. Sin embargo, en Ada se deja explícito, como parámetro formal de subprograma, que «se pasa una función con esta forma».
8.4 Ejemplo de ejecución
Siguiendo la estructura del capítulo 18, si se coloca Generic_Insertion_Sort en un archivo aparte (generic_insertion_sort.ads / .adb), el lado que lo llama queda así.
with Ada.Text_IO; use Ada.Text_IO;
with Generic_Insertion_Sort;
procedure Sort_Demo is
type Int_Array is array (Positive range <>) of Integer;
function Greater (Left, Right : Integer) return Boolean is
(Left > Right);
procedure Sort_Asc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array);
procedure Sort_Desc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array,
"<" => Greater);
procedure Show (Label : String; Items : Int_Array) is
begin
Put (Label);
for V of Items loop
Put (Integer'Image (V));
end loop;
New_Line;
end Show;
Asc : Int_Array := (99, 3, 47, 12);
Desc : Int_Array := (99, 3, 47, 12);
begin
Sort_Asc (Asc);
Sort_Desc (Desc);
Show ("Asc :", Asc);
Show ("Desc:", Desc);
end Sort_Demo;
gnatchop ../src/snippets/03_sort.ada
gnatmake -gnata sort_demo
./sort_demo
Asc : 3 12 47 99
Desc: 99 47 12 3
Se comprueba que los dos procedimientos creados a partir del mismo cuerpo de Generic_Insertion_Sort quedan en orden inverso con solo cambiar la función de comparación. El espacio antes de cada elemento se debe a que 'Image, para tipos enteros, coloca un carácter de espacio antes de los valores no negativos.
9. Categorías de tipo ── escribir un contrato más específico que private
type T is private; es cómodo, pero no permite hacer cualquier cosa. Sobre un tipo private no se pueden dar por hechas, sin más, las cuatro operaciones aritméticas básicas ni las comparaciones de orden. Por eso, en Ada se puede especificar una categoría en el parámetro formal de tipo.
flowchart TB
FormalType[Formal Type] --> Private[private]
FormalType --> Limited[limited private]
FormalType --> Discrete[discrete box: tipo discreto]
FormalType --> Signed[range box: entero con signo]
FormalType --> Modular[mod box: entero modular]
FormalType --> Float[digits box: punto flotante]
FormalType --> Fixed[delta box: punto fijo]
FormalType --> Access[tipo access]
Discrete --> Enum[Tipo enumerado]
Discrete --> Int[Tipo entero]
Float --> F1[Float]
Float --> F2[Long_Float]
Float --> F3[Tipo de punto flotante definido por el usuario]
En el diagrama, box se refiere al <> de Ada (el box compound delimiter). Como Mermaid no puede representar <> directamente, en el diagrama se escribe únicamente box.
9.1 Qué se gana al especificar una categoría
Por ejemplo, para calcular la media o la varianza se necesitan la suma, la resta, la multiplicación y la división. Con un tipo private no se pueden dar por hechas estas operaciones. Por eso, se limita a un tipo de punto flotante.
generic
type Real is digits <>;
type Real_Array is array (Positive range <>) of Real;
package Generic_Statistics is
function Mean (Values : Real_Array) return Real;
function Variance (Values : Real_Array) return Real;
end Generic_Statistics;
Con type Real is digits <>; se sabe que Real es un tipo de punto flotante. Por lo tanto, en el cuerpo del genérico se pueden usar +, -, *, /, etc.
9.2 Cuerpo
package body Generic_Statistics is
function Mean (Values : Real_Array) return Real is
Sum : Real := 0.0;
begin
if Values'Length = 0 then
return 0.0;
end if;
for V of Values loop
Sum := Sum + V;
end loop;
return Sum / Real (Values'Length);
end Mean;
function Variance (Values : Real_Array) return Real is
M : constant Real := Mean (Values);
Sum : Real := 0.0;
begin
if Values'Length = 0 then
return 0.0;
end if;
for V of Values loop
declare
D : constant Real := V - M;
begin
Sum := Sum + D * D;
end;
end loop;
return Sum / Real (Values'Length);
end Variance;
end Generic_Statistics;
9.3 Uso con Float y Long_Float
type Float_Array is array (Positive range <>) of Float;
type Long_Array is array (Positive range <>) of Long_Float;
package Float_Stats is new Generic_Statistics (Float, Float_Array);
package Long_Stats is new Generic_Statistics (Long_Float, Long_Array);
Se puede reutilizar el mismo procesamiento estadístico para tipos de punto flotante con distinta precisión.
flowchart LR
Stats[Generic_Statistics<br/>Real is digits box] --> FS[Float_Stats]
Stats --> LS[Long_Stats]
Stats --> MS[My_Real_Stats]
FS --> FCalc[Mean / Variance con Float]
LS --> LCalc[Mean / Variance con Long_Float]
MS --> MCalc[Mean / Variance con Real definido por el usuario]
9.4 Ejemplo de ejecución
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Float_Text_IO; use Ada.Float_Text_IO;
with Generic_Statistics;
procedure Statistics_Demo is
type Float_Array is array (Positive range <>) of Float;
package Float_Stats is new Generic_Statistics (Float, Float_Array);
Samples : constant Float_Array := (1.0, 2.0, 3.0, 4.0);
begin
Put ("Mean = ");
Put (Float_Stats.Mean (Samples), Fore => 1, Aft => 3, Exp => 0);
New_Line;
Put ("Variance = ");
Put (Float_Stats.Variance (Samples), Fore => 1, Aft => 3, Exp => 0);
New_Line;
end Statistics_Demo;
gnatchop ../src/snippets/04_statistics.ada
gnatmake -gnata statistics_demo
./statistics_demo
Mean = 2.500
Variance = 1.250
Ada.Float_Text_IO es una biblioteca estándar que instancia Ada.Text_IO.Float_IO para Float; al especificar Exp => 0 se obtiene la notación decimal normal en lugar de la notación exponencial. Aft => 3 es el número de dígitos de la parte decimal. Tenga en cuenta que esta Variance no es la varianza muestral, sino la varianza poblacional (se divide entre Values'Length).
9.5 La especificación de categoría como «especificación a nivel de tipo»
La especificación de categoría no es simplemente una sintaxis para hacer callar al compilador. También funciona como una especificación que le comunica al lector «qué exige este componente».
| Procesamiento que se quiere escribir | Tipo formal adecuado | Motivo |
|---|---|---|
| Intercambio, almacenamiento, extracción | private |
Basta con poder asignar |
| Gestión de recursos que no se pueden copiar | limited private |
No presupone la asignación |
| Índice de arreglo, recorrido de estados enumerados | (<>) |
Se pueden usar First, Last, Succ, Pred |
| Suma de enteros, contadores | range <> |
Permite dar por hechas operaciones enteras |
| Máscaras de bits, contadores circulares | mod <> |
Permite dar por hechas operaciones modulares |
| Media, varianza, cálculo numérico | digits <> |
Permite dar por hechas operaciones de punto flotante |
| Precisión fija como importes o magnitudes de control | delta <> |
Permite dar por hechas operaciones de punto fijo |
10. Inyección de predicados ── escribir Count_If al estilo Ada
El parámetro formal de subprograma no solo sirve para funciones de comparación, sino también para predicados. Un predicado es una función que recibe un valor y devuelve un Boolean.
Un papel cercano al de Func<T, bool> en C#, Predicate<T> en Java, o las lambdas y objetos de función de C++, se puede expresar en Ada como un parámetro formal de subprograma del genérico.
10.1 Especificación
generic
type Element is private;
type Index is (<>);
type Array_Type is array (Index range <>) of Element;
with function Predicate (Item : Element) return Boolean;
function Generic_Count_If (Arr : Array_Type) return Natural;
Aquí no se añade is <> a Predicate. Como no existe de forma estándar un predicado visible, el diseño obliga a que el lado que lo usa lo proporcione siempre.
10.2 Cuerpo
function Generic_Count_If (Arr : Array_Type) return Natural is
Count : Natural := 0;
begin
for Item of Arr loop
if Predicate (Item) then
Count := Count + 1;
end if;
end loop;
return Count;
end Generic_Count_If;
El flujo de procesamiento es simple.
flowchart LR
Arr[Arreglo] --> Loop[Recorrer cada elemento]
Loop --> P{"Predicate(Item)?"}
P -->|True| Inc[Incrementar Count]
P -->|False| Skip[No hacer nada]
Inc --> Next[Siguiente elemento]
Skip --> Next
Next --> Result[Devolver Count]
10.3 Conteo de pares y conteo por umbral
type Int_Array is array (Positive range <>) of Integer;
function Is_Even (N : Integer) return Boolean is
(N mod 2 = 0);
function Is_Large (N : Integer) return Boolean is
(N > 50);
function Count_Even is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Even);
function Count_Large is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Large);
A partir de la misma lógica de recorrido, se pueden crear dos funciones que solo difieren en la condición.
flowchart TB
G[Generic_Count_If] --> E[Count_Even<br/>Predicate = Is_Even]
G --> L[Count_Large<br/>Predicate = Is_Large]
Data[12, 7, 88, 3, 56, 91, 44, 19, 62] --> E
Data --> L
E --> ER[Cantidad de números pares]
L --> LR[Cantidad de números mayores que 50]
En este ejemplo, el recorrido del arreglo, la gestión del contador y la devolución del resultado son comunes en todos los casos. Lo único que se inyecta como función es «qué se cuenta». Esta es la forma básica del diseño de orden superior en Ada.
10.4 Ejemplo de ejecución
with Ada.Text_IO; use Ada.Text_IO;
with Generic_Count_If;
procedure Count_If_Demo is
type Int_Array is array (Positive range <>) of Integer;
function Is_Even (N : Integer) return Boolean is
(N mod 2 = 0);
function Is_Large (N : Integer) return Boolean is
(N > 50);
function Count_Even is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Even);
function Count_Large is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Large);
Data : constant Int_Array := (12, 7, 88, 3, 56, 91, 44, 19, 62);
begin
Put_Line ("Even =" & Natural'Image (Count_Even (Data)));
Put_Line ("Large =" & Natural'Image (Count_Large (Data)));
end Count_If_Demo;
gnatchop ../src/snippets/05_filter.ada
gnatmake -gnata count_if_demo
./count_if_demo
Even = 5
Large = 4
De los 9 elementos de Data, los pares son 12, 88, 56, 44 y 62 (5 en total), y los mayores que 50 son 88, 56, 91 y 62 (4 en total).
11. Combinar varios parámetros ── un almacén clave-valor genérico
En los componentes reales, no suele bastar con un solo parámetro de tipo. Es necesario combinar varias condiciones: el tipo de la clave y del valor, la forma de comparar las claves, el número máximo de entradas, etc.
Aquí se toma como ejemplo un sencillo almacén clave-valor de longitud fija.
flowchart TB
KV[Generic_KV_Store] --> K[Key_Type]
KV --> V[Value_Type]
KV --> EQ[Función de coincidencia de clave]
KV --> M[Max_Entries]
KV --> Ops[Put / Get / Contains]
Ops --> Use1[Almacén de valores de configuración]
Ops --> Use2[Caché de pequeña escala]
Ops --> Use3[Diccionario de longitud fija para sistemas embebidos]
11.1 Especificación
generic
type Key_Type is private;
type Value_Type is private;
with function "=" (Left, Right : Key_Type) return Boolean is <>;
Max_Entries : Positive := 50;
package Generic_KV_Store is
procedure Put (Key : Key_Type; Val : Value_Type);
function Get (Key : Key_Type) return Value_Type;
function Contains (Key : Key_Type) return Boolean;
Key_Not_Found : exception;
Store_Full : exception;
end Generic_KV_Store;
Este paquete tiene cuatro parámetros formales.
| Parámetro | Tipo | Papel |
|---|---|---|
Key_Type |
Tipo | El tipo de la clave |
Value_Type |
Tipo | El tipo del valor |
"=" |
Subprograma | La determinación de coincidencia de claves |
Max_Entries |
Valor | El número máximo de entradas |
A Max_Entries se le da un valor por defecto con := 50. Por lo tanto, si no se especifica nada en particular, el almacén tendrá 50 entradas.
11.2 Cuerpo
package body Generic_KV_Store is
subtype Index_Type is Positive range 1 .. Max_Entries;
type Key_Array is array (Index_Type) of Key_Type;
type Value_Array is array (Index_Type) of Value_Type;
type Used_Array is array (Index_Type) of Boolean;
Keys : Key_Array;
Values : Value_Array;
Used : Used_Array := (others => False);
function Find_Index (Key : Key_Type) return Natural is
begin
for I in Index_Type loop
if Used (I) and then Keys (I) = Key then
return I;
end if;
end loop;
return 0;
end Find_Index;
function Find_Free return Natural is
begin
for I in Index_Type loop
if not Used (I) then
return I;
end if;
end loop;
return 0;
end Find_Free;
procedure Put (Key : Key_Type; Val : Value_Type) is
Pos : Natural := Find_Index (Key);
begin
if Pos = 0 then
Pos := Find_Free;
if Pos = 0 then
raise Store_Full;
end if;
Used (Pos) := True;
Keys (Pos) := Key;
end if;
Values (Pos) := Val;
end Put;
function Get (Key : Key_Type) return Value_Type is
Pos : constant Natural := Find_Index (Key);
begin
if Pos = 0 then
raise Key_Not_Found;
end if;
return Values (Pos);
end Get;
function Contains (Key : Key_Type) return Boolean is
begin
return Find_Index (Key) /= 0;
end Contains;
end Generic_KV_Store;
Esta implementación usa búsqueda lineal, así que no está pensada para grandes volúmenes de datos. Sin embargo, es una forma fácil de usar en escenarios donde importan las propiedades de longitud fija, pequeña escala y ausencia de reserva dinámica de memoria.
sequenceDiagram
participant App as Lado que llama
participant Store as instancia de Generic_KV_Store
participant Data as Keys/Values/Used
App->>Store: Put(Key, Value)
Store->>Data: Find_Index(Key)
alt Ya existe la clave
Store->>Data: Values(Pos) := Value
else Clave nueva
Store->>Data: Find_Free
Store->>Data: Keys(Pos) := Key
Store->>Data: Values(Pos) := Value
Store->>Data: Used(Pos) := True
end
App->>Store: Get(Key)
Store->>Data: Find_Index(Key)
Data-->>Store: Pos
Store-->>App: Values(Pos)
11.3 Ejemplo de instanciación
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Generic_KV_Store;
procedure KV_Demo is
package Int_String_Store is new Generic_KV_Store
(Key_Type => Integer,
Value_Type => Unbounded_String,
Max_Entries => 10);
begin
Int_String_Store.Put (1, To_Unbounded_String ("Ada"));
Int_String_Store.Put (2, To_Unbounded_String ("SPARK"));
if Int_String_Store.Contains (1) then
Put_Line ("1 => " & To_String (Int_String_Store.Get (1)));
else
Put_Line ("1 => (not found)");
end if;
-- Put sobre una clave existente sobreescribe el valor
Int_String_Store.Put (1, To_Unbounded_String ("Ada 2022"));
Put_Line ("1 => " & To_String (Int_String_Store.Get (1)));
if Int_String_Store.Contains (9) then
Put_Line ("9 => " & To_String (Int_String_Store.Get (9)));
else
Put_Line ("9 => (not found)");
end if;
end KV_Demo;
gnatchop ../src/snippets/06_kv_store.ada
gnatmake -gnata kv_demo
./kv_demo
1 => Ada
1 => Ada 2022
9 => (not found)
Si se llama a Get sin comprobar antes la existencia con Contains, y la clave no existe, se lanza Key_Not_Found. Elija entre ramificar con Contains como en el ejemplo anterior, o escribir un manejador de excepciones.
Se omite "=". Como Integer tiene el operador de igualdad estándar, is <> hace que se use ese operador.
Si la clave fuera, por ejemplo, una cadena que ignora mayúsculas y minúsculas, se podría pasar una función de igualdad propia.
function Same_Key (Left, Right : Unbounded_String) return Boolean is
(To_Lower (To_String (Left)) = To_Lower (To_String (Right)));
package String_Key_Store is new Generic_KV_Store
(Key_Type => Unbounded_String,
Value_Type => Integer,
"=" => Same_Key,
Max_Entries => 100);
12. Parámetro formal de paquete ── modularizar aún más los genéricos
En los genéricos de Ada, un paquete entero se puede convertir en parámetro formal. Con esto, se puede tratar «una instancia creada a partir de un paquete genérico» como entrada de otro genérico.
flowchart LR
GS[Generic_Stack] --> IS[Int_Stack]
IS --> Logger[Generic_Stack_Logger]
Logger --> Logged[Int_Stack_Logger_Instance]
12.1 Un registrador que recibe una pila
Por ejemplo, supongamos que se crea un registrador (logger) que recibe una instancia del Generic_Stack anterior y muestra su tamaño.
generic
with package Stack is new Generic_Stack (<>);
package Generic_Stack_Logger is
procedure Print_Size;
end Generic_Stack_Logger;
El cuerpo queda de la siguiente manera.
with Ada.Text_IO; use Ada.Text_IO;
package body Generic_Stack_Logger is
procedure Print_Size is
begin
Put_Line ("Stack size =" & Natural'Image (Stack.Size));
end Print_Size;
end Generic_Stack_Logger;
En el lado que lo usa, primero se crea la pila y luego se pasa esa pila al registrador.
package Int_Stack is new Generic_Stack
(Element_Type => Integer,
Max_Size => 10);
package Int_Stack_Logger is new Generic_Stack_Logger
(Stack => Int_Stack);
Con este diseño, los componentes genéricos se pueden combinar entre sí.
flowchart TB
subgraph Layer1[Primera etapa]
T1[Generic_Stack] --> I1[Int_Stack]
end
subgraph Layer2[Segunda etapa]
T2[Generic_Stack_Logger] --> I2[Int_Stack_Logger]
end
I1 --> T2
I2 --> API[Print_Size]
Es un uso cercano a los parámetros de plantilla de plantilla de C++, pero en Ada se puede dejar explícito que «se recibe una instancia de este paquete genérico». En código Ada a gran escala, resulta útil para separar y combinar contenedores, algoritmos, registro, verificación, ayudas de prueba, etc.
13. Contract model ── la idea más importante de los genéricos de Ada
Lo importante para entender los genéricos de Ada es el contract model.
El cuerpo del genérico debe escribirse usando únicamente las operaciones que prometen los parámetros formales. Por ejemplo, aunque solo se haya declarado type Element is private;, no se puede usar < sobre Element. Si se quiere usar <, hace falta dejarlo explícito como parámetro formal de subprograma o especificar una categoría de tipo más concreta.
flowchart TB
Spec[generic formal part<br/>contrato] --> Body[generic body<br/>implementación dentro del contrato]
Body --> Check1[Comprobación de tipo del cuerpo por sí solo]
Spec --> Inst[Instanciación]
Actual[actual parameters<br/>tipo, función y valor reales] --> Inst
Inst --> Check2[Comprobar si el parámetro real cumple el contrato]
Check2 --> Instance[Paquete o subprograma normal]
Este diseño protege tanto a quien usa el genérico como a quien lo escribe.
13.1 Diferencia de enfoque con las plantillas de C++
Las plantillas de C++ son potentes, pero históricamente tenían la característica de que «el error solo aparece al instanciar el cuerpo de la plantilla». Esto se ha mejorado con los concepts de C++20, pero los genéricos de Ada son, desde el principio, un modelo que deja el contrato explícito.
flowchart LR
subgraph Ada[Ada]
A1[Escribir el contrato en formal part] --> A2[body se comprueba dentro del contrato]
A2 --> A3[instantiation comprueba el actual]
end
subgraph CPP[C++ templates]
C1[Escribir template body] --> C2[Los requisitos se concretan en instantiation]
C2 --> C3[Las restricciones se pueden dejar explícitas con concepts]
end
En los genéricos de Java y C#, el centro del diseño gira en torno a los tipos por referencia, las restricciones, el borrado de tipos y su relación con la representación en tiempo de ejecución. Los genéricos de Ada, en cambio, se inclinan hacia la idea de crear instancias concretas en tiempo de compilación.
| Aspecto | Ada | C++ | Java | Rust |
|---|---|---|---|---|
| Cómo se escribe el contrato | Tipo, valor, función y paquete en formal part | templates / concepts | Parámetro de tipo y bounds | trait bounds |
| Comprobación del cuerpo | Dentro del contrato de los parámetros formales | Se centra en la concreción en la instanciación | Dentro de los bounds | Dentro de los trait bounds |
| Costo en tiempo de ejecución | Básicamente resolución estática | Básicamente generación estática | Afectado por el borrado de tipos | Básicamente monomorfización |
| Parámetro de valor | Sí | Sí | Limitado | const generics |
| Subprograma como parámetro formal | Sí | Se expresa con objetos de función, etc. | Lambdas / interfaces funcionales | Closures / funciones / trait |
| Paquete como parámetro formal | Sí | Plantillas de plantilla, etc. | No | Aparte de la estructura de módulos |
Las funciones detalladas de cada lenguaje son distintas, pero la característica de Ada es «escribir primero el contrato como sintaxis».
14. Decisiones de diseño en la práctica ── qué conviene hacer genérico
Los genéricos son cómodos, pero no significa que convenga convertir en genérico cualquier cosa. En la práctica, decidir según el siguiente criterio reduce el riesgo de errores.
flowchart TB
Start[Hay un procesamiento que se quiere reutilizar] --> Q1{Solo cambia el tipo?}
Q1 -->|Yes| GType[Considerar un parámetro de tipo]
Q1 -->|No| Q2{También cambian el tamaño o el umbral?}
Q2 -->|Yes| GObject[Añadir un parámetro de valor]
Q2 -->|No| Q3{Cambia el comportamiento de comparación o de decisión?}
Q3 -->|Yes| GSubp[Añadir un parámetro formal de subprograma]
Q3 -->|No| Q4{Se quiere agrupar estado interno y API?}
Q4 -->|Yes| GPackage[Paquete genérico]
Q4 -->|No| Normal[Basta con un subprograma normal]
14.1 Cuándo conviene un subprograma genérico
El subprograma genérico es adecuado para algoritmos sin estado.
SwapSortCount_IfFind- Una transformación al estilo
Map Min/Max
Cuando el cuerpo del algoritmo es corto y la entrada y la salida son claras, un subprograma resulta más legible que un paquete.
14.2 Cuándo conviene un paquete genérico
El paquete genérico es adecuado cuando se quiere mantener, junto con el tipo, varias operaciones y un estado interno.
- Pila de longitud fija
- Búfer circular
- Diccionario de pequeña escala
- Conjunto de procesamiento estadístico
- Abstracción de E/S por dispositivo
- Conjunto de operaciones para un tipo numérico con sistema de unidades
En particular, en Ada la especificación del paquete es la API pública y el cuerpo del paquete es la implementación, ya que están separados; por eso el paquete genérico se puede usar como «plantilla de módulo con seguridad de tipos».
flowchart LR
Spec[package spec<br/>API pública] --> User[Lado que lo usa]
Body[package body<br/>implementación interna] -.oculto.-> User
Formal[generic formal part<br/>contrato de tipo, valor y función] --> Spec
Formal --> Body
14.3 Empezar con pocos parámetros formales
Si se añaden demasiados parámetros formales, la instanciación se vuelve difícil de leer. Es más seguro empezar con el mínimo y añadir más solo cuando surja una razón para sustituir algo.
-- Ejemplo que tiende a ser difícil de leer
package X is new Generic_Foo
(A, B, C, D, E, F, G);
-- Dejar la intención clara con asociación con nombre
package X is new Generic_Foo
(Element_Type => Integer,
Index_Type => Positive,
Buffer_Size => 128,
"<" => Less);
En Ada se puede usar la asociación con nombre en el momento de la instanciación. Como los puntos importantes del diseño del genérico se manifiestan en la instanciación, en el código de producción suele ser más fácil de mantener escribirla con nombres.
15. Errores frecuentes al empezar
Los genéricos de Ada son potentes, pero hay puntos con los que es fácil tropezar al empezar.
15.1 Con un tipo private no se puede comparar con <
No se puede escribir un cuerpo como el siguiente.
generic
type Element is private;
function Bad_Min (A, B : Element) return Element;
function Bad_Min (A, B : Element) return Element is
begin
if A < B then -- aquí se produce un error
return A;
else
return B;
end if;
end Bad_Min;
Como Element solo se declara private, no se garantiza que se pueda usar <. Si se quiere comparar, hay que añadirlo al contrato como sigue.
generic
type Element is private;
with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
flowchart LR
Need[Se quiere comparar en el cuerpo] --> Contract[Escribir la función de comparación en formal part]
Contract --> OK[Se comprueba la comparabilidad en la instanciación]
Need --> NoContract[Solo private]
NoContract --> Error[Error de compilación en el cuerpo del genérico]
15.2 is <> no es «inferencia automática de cualquier cosa»
is <> es cómodo, pero no es magia. En el punto de instanciación, el operador o subprograma compatible debe ser visible. Si se coloca una función de comparación propia en otro paquete, es más seguro hacer un with o use adecuado, o pasarla explícitamente con nombre.
procedure Sort_By_Age is new Generic_Insertion_Sort
(Item_Type => Person,
Index => Positive,
Item_Array => Person_Array,
"<" => Younger_Than);
15.3 Cada instancia tiene sus propias excepciones
Si se declara una excepción en la especificación de un paquete genérico, se convierte en una excepción distinta en cada instancia.
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
En este caso, Int_Stack.Stack_Overflow y Float_Stack.Stack_Overflow se tratan como excepciones distintas. Si se quiere tratarlas como una excepción común, también conviene considerar un diseño que defina la excepción fuera del genérico.
flowchart TB
Generic[Generic_Stack<br/>declara Stack_Overflow] --> I[Int_Stack.Stack_Overflow]
Generic --> F[Float_Stack.Stack_Overflow]
I -.Excepciones distintas.-> F
15.4 El tamaño del código puede crecer
Los genéricos facilitan evitar indirecciones adicionales en tiempo de ejecución, pero, como se crea una instancia por cada tipo, si el número de instancias es alto, el tamaño del código puede aumentar.
Es la misma compensación que se observa en las plantillas de C++ o en la monomorfización de Rust. En el desarrollo orientado a alta fiabilidad, sistemas embebidos o tiempo real, la idea es gestionar el tamaño de los artefactos generados en la compilación, a cambio de reducir la incertidumbre en tiempo de ejecución.
flowchart LR
Generic[Un solo cuerpo generic] --> I1[Versión Integer]
Generic --> I2[Versión Float]
Generic --> I3[Versión Long_Float]
Generic --> I4[Versión My_Type]
I1 --> Code[Código generado]
I2 --> Code
I3 --> Code
I4 --> Code
Code --> Pros[Facilita evitar la determinación de tipo o el boxing en tiempo de ejecución]
Code --> Cons[Vigilar el aumento de tamaño si hay muchas instancias]
15.5 Cuándo conviene usar limited private
type Element is private; presupone la asignación. Cuando se manejan elementos que no se quieren copiar, como manejadores de archivo, bloqueos o manejadores de dispositivo, conviene considerar limited private.
generic
type Resource is limited private;
with procedure Close (R : in out Resource);
procedure Generic_Use_And_Close (R : in out Resource);
En un diseño que maneja tipos que no se pueden copiar, es más seguro un algoritmo que aplica un procedimiento o un diseño que deja explícita la referencia, en lugar de un contenedor que almacena valores.
16. Pequeño catálogo de patrones de diseño
A partir de aquí se resumen brevemente las formas que se usan con frecuencia en la práctica.
16.1 Ofrecer Min solo para valores comparables
generic
type Element is private;
with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
function Generic_Min (A, B : Element) return Element is
begin
if A < B then
return A;
else
return B;
end if;
end Generic_Min;
flowchart LR
T[Element] --> C[Necesita una función de comparación]
C --> M[Generic_Min]
M --> R[Devuelve el menor]
16.2 Convertir un umbral en parámetro de valor
generic
type Count_Type is range <>;
Threshold : Count_Type;
function Generic_Is_Over (Value : Count_Type) return Boolean;
function Generic_Is_Over (Value : Count_Type) return Boolean is
begin
return Value > Threshold;
end Generic_Is_Over;
El parámetro de valor es adecuado para valores que se quieren fijar como propiedad de la instancia, en lugar de como un valor de configuración en tiempo de ejecución.
16.3 Inyectar el medio de salida
generic
type Element is private;
with procedure Put (Item : Element);
procedure Generic_Print_Twice (Item : Element);
procedure Generic_Print_Twice (Item : Element) is
begin
Put (Item);
Put (Item);
end Generic_Print_Twice;
Con esta forma, se puede cambiar el destino de la salida: la salida estándar, un registro, un búfer de pruebas, etc.
flowchart TB
Print[Generic_Print_Twice] --> Put[Recibe Put como parámetro formal de subprograma]
Put --> Console[Salida por consola]
Put --> Log[Salida a registro]
Put --> Test[Búfer de pruebas]
16.4 No fijar el tipo de índice del arreglo
En Ada, el tipo de índice del arreglo también es una información de tipo importante. En lugar de fijarlo siempre a Positive, convertir también el tipo de índice en parámetro formal cuando haga falta aumenta la reutilización.
generic
type Element is private;
type Index is (<>);
type Array_Type is array (Index range <>) of Element;
procedure Generic_Clear (Arr : in out Array_Type; Value : Element);
Con este diseño se pueden manejar no solo arreglos con índice Positive, sino también arreglos con índice de tipo enumerado.
flowchart LR
Index[Index is discrete] --> Positive[Positive range]
Index --> Day[Tipo enumerado Day]
Index --> State[Tipo enumerado State]
Index --> Slot[Tipo entero propio]
17. Lista de comprobación para una API al estilo Ada
Al escribir un genérico, al final resulta más legible si se revisa desde los siguientes puntos de vista.
flowchart TB
C[Revisión del diseño del genérico] --> C1[Los parámetros formales son mínimos?]
C --> C2[Se dejaron explícitas en formal part las operaciones necesarias?]
C --> C3[Es adecuada la categoría private / range / digits, etc.?]
C --> C4[Se puede instanciar de forma legible con asociación con nombre?]
C --> C5[Se tiene en cuenta el estado o las excepciones por instancia?]
C --> C6[Se puede tolerar el aumento del tamaño del código?]
C --> C7[Se preparó una instancia para pruebas?]
Resumido en texto, queda así.
- Las operaciones que se usan en el cuerpo deben quedar siempre visibles como contrato en los parámetros formales.
- Si basta con
private, useprivate. Si se necesita aritmética, userange <>odigits <>. - Los comportamientos que cambian según el tipo, como la comparación, el hash, la salida o la conversión, conviértalos en parámetros formales de subprograma.
- Si el tamaño o el umbral son una propiedad de la instancia, conviértalos en parámetro de valor.
- Si hay estado, piense primero en un paquete genérico; si no lo hay, en un subprograma genérico.
- En la instanciación, cuantos más argumentos haya, más conviene usar la asociación con nombre.
- Diseñe partiendo de que las excepciones y el estado interno son independientes en cada instancia.
18. Ejemplo de estructura completa de las muestras
Si se separan en archivos las muestras del artículo, una estructura como la siguiente resulta más legible.
flowchart TB
Root[ada-generic-programming] --> Src[src]
Src --> G[generics]
Src --> D[demos]
G --> SwapSpec[generic_swap.ads]
G --> SwapBody[generic_swap.adb]
G --> StackSpec[generic_stack.ads]
G --> StackBody[generic_stack.adb]
G --> SortSpec[generic_insertion_sort.ads]
G --> SortBody[generic_insertion_sort.adb]
G --> StatsSpec[generic_statistics.ads]
G --> StatsBody[generic_statistics.adb]
G --> CountSpec[generic_count_if.ads]
G --> CountBody[generic_count_if.adb]
G --> KVSpec[generic_kv_store.ads]
G --> KVBody[generic_kv_store.adb]
D --> SwapDemo[swap_demo.adb]
D --> StackDemo[stack_demo.adb]
D --> SortDemo[sort_demo.adb]
D --> StatsDemo[statistics_demo.adb]
D --> CountDemo[count_if_demo.adb]
D --> KVDemo[kv_demo.adb]
Para muestras pequeñas de artículo, es cómodo reunirlo todo en un archivo y usar gnatchop, pero si se piensa en la práctica o el mantenimiento a largo plazo, separar la especificación .ads del cuerpo .adb da una estructura más propia de Ada.
19. Resumen ── decidir los límites de la reutilización con el tipo
La programación genérica de Ada no es solo «una función para escribir código independiente del tipo». Más bien, su esencia está en dejar explícito, como contrato de tipos, subprogramas y valores, qué exige un componente reutilizable.
flowchart LR
Contract[Escribir el contrato] --> Generic[Escribir el cuerpo genérico]
Generic --> Instance[Instanciar pasando tipo, valor y función]
Instance --> Safe[Usar con seguridad de tipos]
Safe --> Reuse[Reutilizar sin copiar]
Como se vio en este artículo, en los genéricos de Ada se pueden convertir en parámetro formal las siguientes cosas.
- Tipos
- Valores
- Subprogramas
- Paquetes
Además, para los tipos se pueden especificar categorías bastante detalladas, como private, limited private, range <>, mod <>, digits <>, delta <> o (<>). Gracias a esto, el cuerpo del genérico no depende de «operaciones de las que no se sabe si están disponibles», sino que se puede implementar de forma segura solo con las operaciones escritas en el contrato.
En activos de C o de C++ antiguo, para la reutilización a veces se usan macros, void*, punteros a función o bifurcaciones de tipo escritas a mano. Los genéricos de Ada pueden sustituir gran parte de esos usos por una forma segura en cuanto a tipos y legible. En particular, en software de mantenimiento a largo plazo, sistemas embebidos, tiempo real y alta fiabilidad, este «diseño que decide los límites en tiempo de compilación» tiene un gran valor.
20. Áreas de consultoría relacionadas
KomuraSoft LLC se dedica al desarrollo de aplicaciones Windows, la investigación y modificación de activos existentes, la organización de límites entre COM / ActiveX / 32 bits / 64 bits, así como a la consultoría técnica y la revisión de diseño. Además de diseños orientados al tipado estático y la alta fiabilidad como Ada, en la práctica también es un tema cercano cómo organizar y prolongar la vida útil o migrar activos existentes en C/C++, C#, VB6, MFC o COM.
- Desarrollo de aplicaciones Windows
- Desarrollo de componentes COM
- Modernización de aplicaciones Windows
- Consultoría técnica y revisión de diseño
- Contacto
Referencias
- Ada 2022 Language Reference Manual, Section 12: Generic Units ── Es la regulación misma de las unidades genéricas. 12.1 corresponde a la declaración genérica, 12.3 a la instanciación, 12.4 al objeto formal (parámetro de valor), 12.5 al tipo formal (categorías como
private,range <>,digits <>), 12.6 al subprograma formal y 12.7 al paquete formal. Si tiene dudas en algún capítulo de este artículo, lo más seguro es consultar la sección correspondiente. - Ada 2022 Language Reference Manual, 2.2: Lexical Elements, Separators, and Delimiters ── Es el lugar donde se define que
<>se llama el delimitador compuesto «box». Es el fundamento de la notaciónboxen los diagramas. - GNAT User’s Guide for Native Platforms ── Explica el uso de
gnatmakeygnatchop, y lista las opciones de compilación, empezando por-gnata. Consúltelo si se atasca con los pasos del capítulo 3. - Alire Documentation ── Incluye cómo instalar
alr, cómo gestionar la cadena de herramientas (GNAT / gprbuild) y cómo crear un crate. Si solo se trata de configurar el entorno, este material es suficiente. - Código de muestra (GitHub) ── Son las muestras de este artículo separadas en archivos según la estructura del capítulo 18.
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
El atractivo del lenguaje Ada ── Cuando los tipos expresan el diseño, el lenguaje que sostiene software que funciona durante décadas
Presentamos el atractivo del lenguaje Ada: tipado fuerte, restricciones de rango, paquetes que separan especificación e implementación, d...
Programación de sistemas de tiempo real con Ada — Control práctico de prioridad, periodo y tiempo de ejecución
Aprenda el Annex D de Ada (sistemas de tiempo real) con 8 ejemplos prácticos de código: prioridad de tareas, Ceiling_Locking, ejecución p...
Concurrencia segura en Ada — Guía práctica de tareas y objetos protegidos
Introducción a las tareas y objetos protegidos de Ada: rendezvous, aceptación selectiva, exclusión mutua, llamadas con tiempo de espera y...
Introducción a la verificación formal con SPARK ── De los contratos de Ada a la demostración matemática
Un artículo introductorio y práctico sobre la verificación formal con SPARK, el subconjunto de Ada. Repasa cómo pasar de los contratos (P...
Cómo funcionan el portapapeles y arrastrar y soltar — Gestionar correctamente la transferencia de datos OLE en aplicaciones empresariales
Una tabla de Excel se deforma al pegarla y deja de poder pegarse si cierra el origen: es el portapapeles colocando el mismo contenido en ...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Qué son los genéricos de Ada?
- Es un mecanismo de reutilización que recibe tipos, valores, subprogramas o incluso paquetes como parámetros formales, y que se comprueba estáticamente en el momento de la instanciación mediante new. No es una simple sustitución de texto: en tiempo de compilación se determina si «este componente cumple este contrato». Un subprograma genérico no se puede llamar solo con declararlo; únicamente al instanciarlo con un tipo concreto se convierte en un procedimiento o función normal utilizable.
- ¿En qué se diferencian los genéricos de Ada de las plantillas de C++?
- Ada adopta desde el principio un contract model que exige declarar el contrato de forma explícita: el cuerpo del genérico solo puede usar las operaciones que prometen los parámetros formales, y se comprueba el tipo del cuerpo por sí solo. Las plantillas de C++ tenían históricamente la característica de que los errores solo aparecían al instanciarlas, algo que los concepts de C++20 mejoraron. Además, en Ada, junto con los parámetros de valor y de subprograma, se puede recibir un paquete entero como parámetro formal, lo que permite combinar componentes genéricos entre sí.
- ¿Por qué se especifica una categoría de tipo en el parámetro formal de tipo de Ada?
- Para dejar explícitas, como contrato, las operaciones que se pueden usar en el cuerpo. Con type T is private solo se pueden dar por hechas operaciones básicas como la asignación o la comparación de igualdad; no se pueden usar comparaciones de orden ni operaciones aritméticas. Si se necesita aritmética entera se especifica range <>; si se necesita aritmética de punto flotante, digits <>; y si se necesitan operaciones a nivel de bits, mod <>. La especificación de categoría también funciona como una especificación a nivel de tipo que le dice al lector «qué exige este componente».
- ¿Hay algún punto a tener en cuenta con los genéricos de Ada?
- Como se crea una instancia por cada tipo, si el número de instancias es alto, el tamaño del código puede aumentar. Es la misma compensación que se observa en las plantillas de C++ o en la monomorfización de Rust. Además, una excepción declarada en la especificación de un paquete genérico se convierte en una excepción distinta en cada instancia. Como pauta práctica: empiece con el mínimo de parámetros formales y añada más solo cuando surja una razón concreta para sustituirlos, y use la asociación con nombre cuando una instanciación tenga muchos argumentos.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.