Programación genérica en Ada ── escribir contratos con tipos y lograr la reutilización sin costo en tiempo de ejecución

· Actualizado el: · · 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».

Proceso que se quiere reutilizar¿Cómo reutilizarlo?Copiar y pegarvoid* / Object / conversión de tipoGenérico de AdaEs fácil olvidar alguna correcciónEs fácil que aparezcan errores en tiempo de ejecución o pérdida de seguridad de tiposSeguro en cuanto a tiposComprobación en tiempo de compilaciónSin 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_If y 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.

Genéricos de AdaSubprograma genéricoSwapCount_IfSortPaquete genéricoStackStatisticsKV StoreParámetros formalesTipoprivatelimited privaterange boxmod boxdigits boxdelta boxdiscrete boxObjetoMax_SizeThresholdSubprogramafunción Lessfunción EqualsPredicatePaquetewith package P is newGenericIdeas de diseñoContract ModelComprobación estáticaAbstracción sin costoSeparación deespecificació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.

ada-generic-programming/src/snippets/01_swap.ada02_stack.ada03_sort.ada04_statistics.ada05_filter.ada06_kv_store.adaREADME.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.

EjecutablegnatmakegnatchopDesarrollador/aEjecutablegnatmakegnatchopDesarrollador/aPasar un único archivo .adaDividir en .ads / .adb / maingnatmake -gnata mainEjecutar hasta el enlace, incluido el binding./mainResultado 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.

  1. Escribir la unidad genérica
  2. Escribir los parámetros formales en la parte generic
  3. Instanciar con new en el lado que la usa
declaración genericparámetros formalescuerpo genéricoinstanciación con newuso como subprograma o paquete normalTipoValorSubprogramaPaquete

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.

se pasa Integerse pasa Characterse pasa My_RecordGeneric_Swaptype Element is privateSwap_IntegerSwap_CharacterSwap_My_RecordIntercambia variables IntegerIntercambia variables CharacterIntercambia 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.

generic formal parametersParámetro de tipoParámetro de objeto / valorParámetro de subprogramaParámetro de paquetetype Element is privatetype Index is boxtype Real is digits boxMax_Size : PositiveDefault_Value : Elementwith function Less...with procedure Put ...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_Swap se puede usar con cualquier tipo Element
  • El cuerpo no depende de la estructura interna de Element ni 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.

Después de la llamadaAntes de la llamadaA = 20B = 10A = 10B = 20Temp = A

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.

Generic_StackElement_TypeMax_SizePush / Pop / Size / Is_Empty / Is_FullInt_StackElement=IntegerMax_Size=5Float_StackElement=FloatMax_Size=3String_StackElement=Unbounded_StringMax_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_Type es un parámetro de tipo
  • Max_Size es 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.

Estado de Float_StackTopData : arreglo de FloatEstado de Int_StackTopData : arreglo de IntegerGeneric_StackInt_StackFloat_Stack

7.3 Transiciones de estado de la pila

La pila resulta fácil de entender si se ve como una máquina de estados.

PushPush / PopPop extrae el último elementoPush alcanza Max_SizePopPushPopEmptyNonEmptyFullOverflowUnderflow

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.

Generic_Insertion_SortItem_TypeIndexItem_ArrayFunción de comparaciónUsar la comparación estándarPasar Greater para orden descendentePasar 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.

  1. Item_Type: el tipo de los elementos del arreglo
  2. Index: el tipo del índice del arreglo
  3. Item_Array: el tipo de arreglo real
  4. "<": 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.

YesNoNoYesArreglo sin ordenarExtraer Key de izquierda a derechaKey va antes que el elemento anterior?Desplazar el elemento anterior a la derechaInsertar KeySe procesó hasta el final?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.

99, 3, 47, 12Sort_Asccomparación estándarSort_Descpasa Greater como función de comparación3, 12, 47, 9999, 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.

Formal Typeprivatelimited privatediscrete box: tipo discretorange box: entero con signomod box: entero modulardigits box: punto flotantedelta box: punto fijotipo accessTipo enumeradoTipo enteroFloatLong_FloatTipo 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.

Generic_StatisticsReal is digits boxFloat_StatsLong_StatsMy_Real_StatsMean / Variance con FloatMean / Variance con Long_FloatMean / 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.

TrueFalseArregloRecorrer cada elementoPredicate(Item)?Incrementar CountNo hacer nadaSiguiente elementoDevolver 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.

Generic_Count_IfCount_EvenPredicate = Is_EvenCount_LargePredicate = Is_Large12, 7, 88, 3, 56, 91, 44, 19, 62Cantidad de números paresCantidad 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.

Generic_KV_StoreKey_TypeValue_TypeFunción de coincidencia de claveMax_EntriesPut / Get / ContainsAlmacén de valores de configuraciónCaché de pequeña escalaDiccionario 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.

Keys/Values/Usedinstancia de Generic_KV_StoreLado que llamaKeys/Values/Usedinstancia de Generic_KV_StoreLado que llamaalt[Ya existe la clave][Clave nueva]Put(Key, Value)Find_Index(Key)Values(Pos) := ValueFind_FreeKeys(Pos) := KeyValues(Pos) := ValueUsed(Pos) := TrueGet(Key)Find_Index(Key)PosValues(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.

Generic_StackInt_StackGeneric_Stack_LoggerInt_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í.

Segunda etapaPrimera etapaInt_Stack_LoggerGeneric_Stack_LoggerInt_StackGeneric_StackPrint_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.

generic formal partcontratogeneric bodyimplementación dentro del contratoComprobación de tipo del cuerpo por sí soloInstanciaciónactual parameterstipo, función y valor realesComprobar si el parámetro real cumple el contratoPaquete 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.

C++ templatesLos requisitos se concretan en instantiationEscribir template bodyLas restricciones se pueden dejar explícitas con conceptsAdabody se comprueba dentro del contratoEscribir el contrato en formal partinstantiation comprueba el actual

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 Limitado const generics
Subprograma como parámetro formal Se expresa con objetos de función, etc. Lambdas / interfaces funcionales Closures / funciones / trait
Paquete como parámetro formal 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.

YesNoYesNoYesNoYesNoHay un procesamiento que se quiere reutilizarSolo cambia el tipo?Considerar un parámetro de tipoTambién cambian el tamaño o el umbral?Añadir un parámetro de valorCambia el comportamiento de comparación o de decisión?Añadir un parámetro formal de subprogramaSe quiere agrupar estado interno y API?Paquete genéricoBasta con un subprograma normal

14.1 Cuándo conviene un subprograma genérico

El subprograma genérico es adecuado para algoritmos sin estado.

  • Swap
  • Sort
  • Count_If
  • Find
  • 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».

ocultpackage specAPI públicaLado que lo usapackage bodyimplementación internageneric formal partcontrato de tipo, valor y función

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;
Se quiere comparar en el cuerpoEscribir la función de comparación en formal partSe comprueba la comparabilidad en la instanciaciónSolo privateError 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.

Excepciones distintasGeneric_Stackdeclara Stack_OverflowInt_Stack.Stack_OverflowFloat_Stack.Stack_Overflow

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.

Un solo cuerpo genericVersión IntegerVersión FloatVersión Long_FloatVersión My_TypeCódigo generadoFacilita evitar la determinación de tipo o el boxing en tiempo de ejecuciónVigilar 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;
ElementNecesita una función de comparaciónGeneric_MinDevuelve 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.

Generic_Print_TwiceRecibe Put como parámetro formal de subprogramaSalida por consolaSalida a registroBú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.

Index is discretePositive rangeTipo enumerado DayTipo enumerado StateTipo 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.

Revisión del diseño del genéricoLos parámetros formales son mínimos?Se dejaron explícitas en formal part las operaciones necesarias?Es adecuada la categoría private / range / digits, etc.?Se puede instanciar de forma legible con asociación con nombre?Se tiene en cuenta el estado o las excepciones por instancia?Se puede tolerar el aumento del tamaño del código?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, use private. Si se necesita aritmética, use range <> o digits <>.
  • 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.

ada-generic-programmingsrcgenericsdemosgeneric_swap.adsgeneric_swap.adbgeneric_stack.adsgeneric_stack.adbgeneric_insertion_sort.adsgeneric_insertion_sort.adbgeneric_statistics.adsgeneric_statistics.adbgeneric_count_if.adsgeneric_count_if.adbgeneric_kv_store.adsgeneric_kv_store.adbswap_demo.adbstack_demo.adbsort_demo.adbstatistics_demo.adbcount_if_demo.adbkv_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.

Escribir el contratoEscribir el cuerpo genéricoInstanciar pasando tipo, valor y funciónUsar con seguridad de tiposReutilizar 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.

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ón box en los diagramas.
  • GNAT User’s Guide for Native Platforms ── Explica el uso de gnatmake y gnatchop, 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 recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

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.

Volver al blog