Ada práctico: arreglos, contenedores, cadenas y argumentos de línea de comandos

Por: Artiko
sistemas-operativosadaelixirconcurrenciaarregloscontenedorescadenaslinea-de-comandosentrada-salida

Ada práctico: arreglos, contenedores, cadenas y argumentos de línea de comandos

En el capítulo 12 dejamos instalado el entorno, compilamos un “hola mundo”, declaramos tipos escalares con rangos propios y escribimos programas que calculan una raíz cuadrada y convierten temperaturas. Todo eso trabajaba con un valor a la vez: un entero, un flotante, un booleano. Un programa real casi nunca opera sobre un valor suelto; opera sobre colecciones de valores, sobre texto, y sobre datos que le llegan desde afuera. Este capítulo cubre exactamente ese salto: de los tipos escalares a los tipos compuestos, del programa que se ejecuta siempre igual al programa que responde a lo que le pasa el shell, y de la salida por pantalla al diálogo con una persona.

El hilo conductor es un programa concreto: el clásico juego de adivinar un número. Es un ejercicio pequeño pero honesto, porque para escribirlo bien hacen falta cuatro cosas que corresponden a las cuatro secciones de este capítulo. Un arreglo o un contenedor para guardar el historial de intentos. Cadenas de texto para leer lo que escribe el usuario y para armar mensajes. Argumentos de línea de comandos para configurar el rango del sorteo sin recompilar. Y un manejo cuidadoso de entrada, porque el usuario va a escribir cosas que no son números y el programa no puede caerse por eso.

Además, cada una de estas piezas tiene un vínculo directo con el sistema operativo. Un arreglo es un bloque contiguo de memoria dentro del espacio de direcciones que el kernel le dio al proceso. Una cadena de tamaño dinámico pide memoria al montón, que crece con llamadas al sistema. Los argumentos de línea de comandos no los inventa Ada: llegan porque el shell hizo fork y después execve pasando un arreglo de punteros a cadenas. Y el código de salida que devuelve el programa es lo que el shell lee para decidir si un && continúa. Vamos a ir señalando esos vínculos a medida que aparezcan.

Del tipo escalar al tipo compuesto

Ada divide los tipos en dos grandes familias. Los escalares contienen un solo valor: enteros, reales, enumeraciones, caracteres. Los compuestos agrupan varios valores: arreglos, registros, tipos protegidos, tipos etiquetados. La diferencia práctica es que un tipo compuesto tiene partes accesibles individualmente, y que su tamaño puede depender de información que solo se conoce en tiempo de ejecución.

Lo que distingue a Ada de la mayoría de los lenguajes en este terreno no es que tenga arreglos, sino cuánta información sobre esos arreglos vive en el sistema de tipos. En C, int v[10] y int w[20] son objetos del mismo tipo int[] en cuanto los pasas a una función: el tamaño se pierde y hay que llevarlo aparte. En Ada el tamaño viaja con el objeto, se puede consultar con atributos y el compilador o el runtime verifican que ningún índice se salga del rango. Ese chequeo es la razón por la que Ada casi no sufre desbordamientos de búfer, que en C siguen siendo la principal fuente de vulnerabilidades de ejecución remota de código.

flowchart TD
    A["Tipos en Ada"] --> B["Escalares"]
    A --> C["Compuestos"]
    B --> B1["Integer, Float, Boolean"]
    B --> B2["Enumeraciones"]
    B --> B3["Character, Wide_Character"]
    C --> C1["Arreglos: array"]
    C --> C2["Registros: record"]
    C --> C3["Tipos protegidos: protected"]
    C1 --> D1["Restringidos: bordes fijos"]
    C1 --> D2["No restringidos: bordes por objeto"]
    D2 --> E1["String = array de Character"]
    D2 --> E2["Wide_Wide_String"]
    C2 --> F1["Contenedores estándar construidos sobre records y punteros"]

Arreglos: la estructura de datos con contrato

Un arreglo en Ada es una colección de elementos del mismo tipo, indexada por un tipo discreto. Esa segunda parte es importante y suele sorprender a quien viene de C o de Python: el índice no tiene por qué ser un entero que empieza en cero. Puede empezar en el número que quieras, puede ser un rango de enteros negativos, y puede ser una enumeración.

Declaración básica

La forma mínima declara un tipo de arreglo y después un objeto de ese tipo.

with Ada.Text_IO;

procedure Arreglos_Basico is

   --  Un tipo de arreglo: cinco enteros, índices del 1 al 5.
   type Numeros is array (1 .. 5) of Integer;

   Mis_Numeros : Numeros;

begin
   Mis_Numeros (1) := 10;
   Mis_Numeros (2) := 20;
   Mis_Numeros (3) := 30;
   Mis_Numeros (4) := 40;
   Mis_Numeros (5) := 50;

   for I in 1 .. 5 loop
      Ada.Text_IO.Put_Line (Integer'Image (Mis_Numeros (I)));
   end loop;
end Arreglos_Basico;

Dos detalles de sintaxis que conviene fijar de inmediato. El acceso a un elemento usa paréntesis, no corchetes: Mis_Numeros (3). Esto es deliberado, porque en Ada un arreglo y una función son intercambiables desde el punto de vista del que los usa: ambos son “algo a lo que le das un índice y te devuelve un valor”. Y la asignación usa :=, mientras que = es la comparación de igualdad.

El atributo 'Image convierte un valor escalar en su representación textual. Integer'Image (10) devuelve " 10" con un espacio adelante, que es el lugar reservado para el signo. Desde Ada 2022 también se puede escribir Mis_Numeros (I)'Image directamente sobre el objeto, y GNAT lo acepta desde bastante antes.

Índices que no empiezan en uno

Nada obliga a que el rango sea 1 .. N. Estas declaraciones son todas legales:

with Ada.Text_IO;

procedure Indices_Variados is

   --  Índices desde cero, como en C; o negativos, alrededor de un centro.
   type Buffer  is array (0 .. 255) of Integer;
   type Ventana is array (-3 .. 3) of Float;

   --  Índice de tipo enumerado: cada día tiene su casilla.
   type Dia is (Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo);
   type Horas_Trabajadas is array (Dia) of Natural;

   Semana : constant Horas_Trabajadas :=
     (Lunes .. Viernes => 8, Sabado | Domingo => 0);

begin
   for D in Dia loop
      Ada.Text_IO.Put_Line
        (Dia'Image (D) & ":" & Natural'Image (Semana (D)));
   end loop;
end Indices_Variados;

El arreglo indexado por enumeración elimina de raíz una clase completa de errores. No existe la posibilidad de escribir Semana (7) y salirse: el único valor que el compilador acepta como índice es un valor del tipo Dia. Es la misma idea que vimos en el capítulo anterior con los subtipos de rango, aplicada al indexado.

Atributos de arreglo

Un arreglo lleva consigo sus propios límites, y se consultan con atributos. Esto reemplaza al parámetro size_t n que en C hay que arrastrar a mano.

AtributoQué devuelveEjemplo sobre array (1 .. 5)
A'Firstprimer índice válido1
A'Lastúltimo índice válido5
A'Lengthcantidad de elementos5
A'Rangeel rango A'First .. A'Last1 .. 5
A'First (N)primer índice de la dimensión Npara matrices
A'Last (N)último índice de la dimensión Npara matrices
A'Length (N)cantidad de elementos de la dimensión Npara matrices

La forma idiomática de recorrer un arreglo en Ada no es for I in 1 .. 5, sino for I in A'Range. Si mañana cambias la declaración del tipo, el bucle sigue siendo correcto sin tocarlo.

with Ada.Text_IO;

procedure Recorrido is

   type Notas is array (1 .. 6) of Float;

   Curso : constant Notas := (5.5, 6.2, 4.8, 7.0, 3.9, 6.6);

   Suma  : Float := 0.0;
   Mayor : Float := Curso (Curso'First);

begin
   --  Recorrido por índice: sirve cuando necesitas la posición.
   for I in Curso'Range loop
      Suma := Suma + Curso (I);
      if Curso (I) > Mayor then
         Mayor := Curso (I);
      end if;
   end loop;

   --  Recorrido por elemento: más limpio cuando la posición no importa.
   for Nota of Curso loop
      Ada.Text_IO.Put_Line ("Nota:" & Float'Image (Nota));
   end loop;

   Ada.Text_IO.Put_Line
     ("Promedio:" & Float'Image (Suma / Float (Curso'Length)));
   Ada.Text_IO.Put_Line ("Máxima:" & Float'Image (Mayor));
end Recorrido;

Float (Curso'Length) es una conversión de tipo explícita. Ada no convierte entre enteros y reales de forma automática; si mezclas los dos sin decirlo, el compilador rechaza el programa. Es una molestia el primer día y un salvavidas después, porque la aritmética entera silenciosa que trunca resultados es una fuente clásica de errores numéricos.

Agregados: inicializar en un solo paso

Un agregado es la notación para construir un valor compuesto completo. Hay tres formas y se pueden combinar.

with Ada.Text_IO;

procedure Agregados is

   type Marcador is array (1 .. 8) of Integer;

   --  Posicional: los valores van en orden.
   A : constant Marcador := (10, 20, 30, 40, 50, 60, 70, 80);

   --  Nombrado: cada valor indica su índice o su rango de índices.
   B : constant Marcador := (1 .. 4 => 0, 5 .. 8 => 1);

   --  Con "others": lo que no se nombró toma este valor.
   C : constant Marcador := (1 => 100, 8 => 900, others => 0);

   --  Inicializar todo el arreglo con el mismo valor.
   D : Marcador := (others => 0);

begin
   for I in Marcador'Range loop
      Ada.Text_IO.Put_Line
        (Integer'Image (I) & " ->"
         & Integer'Image (A (I)) & Integer'Image (B (I))
         & Integer'Image (C (I)) & Integer'Image (D (I)));
   end loop;

   --  Un agregado también sirve para reasignar el arreglo completo.
   D := (others => 7);
end Agregados;

Un agregado posicional debe cubrir todas las posiciones del arreglo; no se puede dejar la mitad sin dar. Esa regla evita el caso de C donde int v[8] = {1, 2}; deja seis elementos en cero por una convención que hay que recordar, y el caso opuesto donde una inicialización parcial deja basura. En Ada, o das todos los valores, o dices explícitamente others => X.

Ada 2022 agrega corchetes como notación alternativa de agregado ([1, 2, 3]), pensada sobre todo para los contenedores. Los paréntesis siguen siendo válidos y son lo que vas a encontrar en la mayor parte del código existente.

Arreglos no restringidos

Hasta acá los tipos tenían los bordes fijos en la declaración del tipo. Un arreglo no restringido deja los bordes abiertos con la notación range <>, conocida informalmente como “caja”. Cada objeto de ese tipo fija sus propios bordes en el momento de declararse.

with Ada.Text_IO;

procedure No_Restringidos is

   --  El tipo dice: índices de tipo Positive, bordes por definir.
   type Serie is array (Positive range <>) of Integer;

   --  Cada objeto elige su tamaño.
   Corta : Serie (1 .. 3) := (1, 2, 3);
   Larga : Serie (1 .. 10) := (others => 0);

   --  Los bordes también pueden deducirse del agregado.
   Deducida : constant Serie := (5, 10, 15, 20);

   --  Una función que acepta CUALQUIER tamaño y devuelve otro arreglo.
   function Duplicar (V : Serie) return Serie is
      Resultado : Serie (V'Range);
   begin
      for I in V'Range loop
         Resultado (I) := V (I) * 2;
      end loop;
      return Resultado;
   end Duplicar;

   function Sumar (V : Serie) return Integer is
      Total : Integer := 0;
   begin
      for Elemento of V loop
         Total := Total + Elemento;
      end loop;
      return Total;
   end Sumar;

begin
   Larga := (1, 1, 2, 3, 5, 8, 13, 21, 34, 55);

   Ada.Text_IO.Put_Line ("Suma corta:" & Integer'Image (Sumar (Corta)));
   Ada.Text_IO.Put_Line ("Suma larga:" & Integer'Image (Sumar (Larga)));
   Ada.Text_IO.Put_Line ("Suma deducida:" & Integer'Image (Sumar (Deducida)));

   for Elemento of Duplicar (Corta) loop
      Ada.Text_IO.Put (Integer'Image (Elemento));
   end loop;
   Ada.Text_IO.New_Line;
end No_Restringidos;

Esto es lo que hace que Ada pueda tener funciones genéricas sobre arreglos sin necesidad de plantillas ni de punteros sueltos. Sumar recibe el arreglo completo, con sus bordes, y los consulta con atributos. No hay forma de que el llamador mienta sobre el tamaño, porque el tamaño no es un argumento separado que se pueda equivocar.

El tipo predefinido String es exactamente esto: type String is array (Positive range <>) of Character. Todo lo que aprendiste sobre arreglos no restringidos aplica tal cual a las cadenas.

Rebanadas

Una rebanada (slice) es una porción contigua de un arreglo unidimensional, y en Ada es un valor de primera clase: se puede leer, asignar y pasar a funciones.

with Ada.Text_IO;

procedure Rebanadas is

   type Serie is array (Positive range <>) of Integer;

   Datos : Serie (1 .. 10) := (10, 20, 30, 40, 50, 60, 70, 80, 90, 100);
   Texto : constant String := "Hola, mundo";

begin
   for Elemento of Datos (3 .. 6) loop      --  leer una rebanada
      Ada.Text_IO.Put (Integer'Image (Elemento));
   end loop;
   Ada.Text_IO.New_Line;

   Datos (1 .. 3) := (0, 0, 0);             --  asignar sobre una rebanada

   Ada.Text_IO.Put_Line (Texto (1 .. 4));   --  Hola
   Ada.Text_IO.Put_Line (Texto (7 .. 11));  --  mundo

   declare
      Cola : String renames Texto (7 .. 11);
   begin
      Ada.Text_IO.Put_Line
        ("Primer índice de la cola:" & Integer'Image (Cola'First));
   end;
end Rebanadas;

El último bloque muestra algo que confunde al principio: la rebanada Texto (7 .. 11) tiene 'First = 7, no 1. Los índices no se renumeran. Si necesitas una copia con índices desde uno, asígnala a un objeto nuevo: declare Copia : constant String := Texto (7 .. 11); produce una cadena con bordes 1 .. 5.

La palabra reservada renames crea un nombre alternativo para un objeto existente, sin copiarlo. Es la forma segura de tener una referencia en Ada sin usar punteros.

Arreglos de varias dimensiones

Ada tiene arreglos multidimensionales reales, no arreglos de arreglos disfrazados. Se declaran separando los rangos con coma y se indexan igual: type Tablero is array (1 .. 3, 1 .. 3) of Character; se recorre con for I in Juego'Range (1) loop for J in Juego'Range (2) loop ... Juego (I, J), y su agregado de inicialización completa es (others => (others => ' ')).

Distinto es un arreglo de arreglos, que se declara en dos pasos (type Fila is array (1 .. 3) of Integer; type Tabla is array (1 .. 2) of Fila;) y se indexa T (I) (J). La matriz es un único bloque contiguo de nueve elementos y solo admite los atributos por dimensión que vimos en la tabla; el arreglo de arreglos permite además rebanar y asignar filas completas como valores.

Anatomía de un arreglo en memoria

flowchart LR
    subgraph decl["Declaración del tipo"]
      T["type Serie is array<br/>Positive range &lt;&gt; of Integer"]
    end
    subgraph obj["Objeto en el stack del proceso"]
      B1["borde inferior: 1"]
      B2["borde superior: 5"]
      E0["elemento 1"]
      E1["elemento 2"]
      E2["elemento 3"]
      E3["elemento 4"]
      E4["elemento 5"]
    end
    T --> obj
    B1 --> E0
    E0 --> E1 --> E2 --> E3 --> E4
    subgraph chk["Verificación en tiempo de ejecución"]
      C1["índice pedido"]
      C2{"¿está entre<br/>borde inferior y superior?"}
      C3["acceso al elemento"]
      C4["Constraint_Error"]
    end
    C1 --> C2
    C2 -->|"sí"| C3
    C2 -->|"no"| C4
    B2 -.->|"alimenta la comparación"| C2

Cuando pasas un arreglo no restringido a una función, lo que viaja es un descriptor: los bordes más la dirección de los datos, o una copia completa según el modo del parámetro y la decisión del compilador. Ese descriptor es lo que hace posible V'Length dentro de la función. En C tendrías que pasar el largo aparte, y si mientes, el programa lee memoria ajena sin que nadie se entere hasta que sea tarde.

La verificación de índice tiene un costo. GNAT la puede desactivar con pragma Suppress (Index_Check) o con la opción de compilación correspondiente, y los perfiles de alta integridad definen bajo qué condiciones eso es aceptable. La postura por defecto es dejarla activa: un Constraint_Error visible en desarrollo es infinitamente más barato que una corrupción silenciosa de memoria en producción.

Cuando el tamaño no se conoce: los contenedores estándar

Un arreglo tiene un tamaño fijo desde que el objeto existe. Eso está bien cuando sabes cuántos elementos vas a tener, y es lo correcto en sistemas embebidos donde la asignación dinámica está prohibida. Pero para un programa interactivo que acumula intentos del usuario, necesitas algo que crezca.

Ada define en Ada.Containers una biblioteca de contenedores genéricos que cubre lo mismo que la STL de C++ o java.util. Se instancian con los tipos concretos que quieras usar, y esa instanciación ocurre en tiempo de compilación.

Vectores

Un Vector es un arreglo que crece. Internamente reserva memoria del montón y la reasigna cuando se llena.

with Ada.Text_IO;
with Ada.Containers.Vectors;

procedure Uso_Vectores is

   package Enteros is new Ada.Containers.Vectors
     (Index_Type   => Positive,
      Element_Type => Integer);

   use Enteros;

   V : Vector;

begin
   --  Agregar al final.
   V.Append (10);
   V.Append (20);
   V.Append (30);

   --  Insertar en una posición concreta.
   V.Insert (Before => 2, New_Item => 15);

   Ada.Text_IO.Put_Line
     ("Cantidad:" & Ada.Containers.Count_Type'Image (V.Length));
   Ada.Text_IO.Put_Line
     ("Primer índice:" & Positive'Image (V.First_Index));
   Ada.Text_IO.Put_Line
     ("Último índice:" & Positive'Image (V.Last_Index));

   --  Acceso y modificación por índice, con verificación de rango.
   Ada.Text_IO.Put_Line ("Elemento 2:" & Integer'Image (V (2)));
   V.Replace_Element (Index => 1, New_Item => 99);

   --  Recorrido por elemento.
   for E of V loop
      Ada.Text_IO.Put (Integer'Image (E));
   end loop;
   Ada.Text_IO.New_Line;

   --  Eliminar y vaciar.
   V.Delete (Index => 1);
   V.Clear;
   Ada.Text_IO.Put_Line ("¿Vacío? " & Boolean'Image (V.Is_Empty));
end Uso_Vectores;

V.Length devuelve un valor de tipo Ada.Containers.Count_Type, no Integer. Si lo quieres mezclar con enteros, hay que convertirlo: Integer (V.Length). Es la misma disciplina de conversiones explícitas de siempre.

La notación V.Append (10) es la notación de objeto prefijado, disponible desde Ada 2005 para tipos etiquetados. Equivale a Append (V, 10) y las dos formas son válidas.

Listas doblemente enlazadas

Cuando lo que necesitas es insertar y borrar en el medio con frecuencia, y no te importa perder el acceso por índice, una lista enlazada cuesta menos que un vector.

with Ada.Text_IO;
with Ada.Containers.Doubly_Linked_Lists;

procedure Uso_Listas is

   package Listas_Enteros is new Ada.Containers.Doubly_Linked_Lists
     (Element_Type => Integer);

   use Listas_Enteros;

   L : List;
   C : Cursor;

begin
   L.Append (30);
   L.Append (40);
   L.Prepend (20);
   L.Prepend (10);

   --  Recorrido con cursor: permite insertar o borrar durante el paso.
   C := L.First;
   while Has_Element (C) loop
      if Element (C) = 30 then
         L.Insert (Before => C, New_Item => 25);
      end if;
      Next (C);
   end loop;

   --  Recorrido en sentido inverso.
   C := L.Last;
   while Has_Element (C) loop
      Ada.Text_IO.Put (Integer'Image (Element (C)));
      Previous (C);
   end loop;
   Ada.Text_IO.New_Line;
end Uso_Listas;

Un Cursor es la abstracción de “posición dentro del contenedor”. Es más seguro que un puntero crudo: el contenedor lleva la cuenta de sus modificaciones y, si intentas usar un cursor que quedó inválido por una operación estructural, obtienes una excepción en lugar de leer memoria liberada.

Mapas

Cuando la clave no es una posición sino un valor arbitrario, la estructura adecuada es un mapa. Ada.Containers.Ordered_Maps se instancia con Key_Type y Element_Type, mantiene las claves ordenadas y exige que el tipo de clave tenga definido el operador < (Unbounded_String lo tiene, así que sirve tal cual como clave de texto). Sus operaciones centrales son Insert, Find, Element, Replace_Element y Delete, y se recorre con cursor usando First, Has_Element, Key, Element y Next. Su hermano Ada.Containers.Hashed_Maps pide además una función de dispersión y una de igualdad, no garantiza orden de recorrido y a cambio busca en tiempo constante promedio.

Cómo elegir la estructura

flowchart TD
    A["Necesito guardar una colección"] --> B{"¿Sé el tamaño<br/>al declarar?"}
    B -->|"sí, y es fijo"| C["array restringido"]
    B -->|"sí, pero varía por objeto"| D["array no restringido"]
    B -->|"no, crece en ejecución"| E{"¿Acceso por posición<br/>o por clave?"}
    E -->|"posición"| F{"¿Inserto en el medio<br/>con frecuencia?"}
    E -->|"clave"| G{"¿Necesito recorrer<br/>en orden de clave?"}
    F -->|"no"| H["Ada.Containers.Vectors"]
    F -->|"sí"| I["Ada.Containers.Doubly_Linked_Lists"]
    G -->|"sí"| J["Ada.Containers.Ordered_Maps"]
    G -->|"no, solo búsqueda"| K["Ada.Containers.Hashed_Maps"]
    C --> L["Sin montón: apto para embebidos"]
    D --> L
    H --> M["Usa el montón del proceso"]
    I --> M
    J --> M
    K --> M
EstructuraTamañoAcceso por índiceInsertar al medioUsa montónCuándo conviene
array restringidofijo en el tipoO(1)no aplicanotamaño conocido y constante
array no restringidofijo por objetoO(1)no aplicanofunciones que aceptan cualquier largo
VectorsdinámicoO(1)O(n)lista que crece por el final
Doubly_Linked_Listsdinámicono hayO(1) con cursorinserciones y borrados frecuentes
Ordered_Mapsdinámicopor clave, O(log n)no aplicadiccionario recorrido en orden
Hashed_Mapsdinámicopor clave, O(1) mediono aplicadiccionario solo para búsqueda

Para sistemas de tiempo real y embebidos, Ada define además contenedores acotados (Ada.Containers.Bounded_Vectors y compañía), que reservan su capacidad máxima por adelantado y nunca tocan el montón. Es la forma de tener una API de contenedor sin asignación dinámica impredecible.

Cadenas de texto en Ada

Ada trata el texto con la misma seriedad que el resto de los tipos, y eso significa que hay más de un tipo de cadena. La razón es que “cadena de texto” esconde tres decisiones independientes: qué tan ancho es un carácter, dónde vive la memoria y si el largo puede cambiar.

Caracteres

Hay tres tipos de carácter predefinidos, y cada uno tiene su tipo de cadena correspondiente.

Tipo de carácterAnchoRepertorioTipo de cadena
Character8 bitsISO/IEC 8859-1, o Latin-1String
Wide_Character16 bitsplano multilingüe básico de UnicodeWide_String
Wide_Wide_Character32 bitsUnicode completo, incluidos emojiWide_Wide_String

Los literales de carácter van entre comillas simples y los de cadena entre comillas dobles.

with Ada.Text_IO;

procedure Caracteres is
   Letra   : constant Character := 'A';
   Digito  : constant Character := '7';
   Salto   : constant Character := ASCII.LF;
   Comilla : constant Character := '"';
begin
   Ada.Text_IO.Put_Line ("Letra: " & Letra);
   Ada.Text_IO.Put_Line
     ("Posición de A en la tabla:" & Integer'Image (Character'Pos (Letra)));
   Ada.Text_IO.Put_Line
     ("Carácter en la posición 66: " & Character'Val (66));
   Ada.Text_IO.Put_Line
     ("¿Es dígito? " & Boolean'Image (Digito in '0' .. '9'));
   Ada.Text_IO.Put ("Antes del salto");
   Ada.Text_IO.Put (Salto);
   Ada.Text_IO.Put_Line ("Después del salto");
   Ada.Text_IO.Put_Line ("Una comilla: " & Comilla);
end Caracteres;

Los atributos 'Pos y 'Val convierten entre un valor de un tipo discreto y su posición numérica. Funcionan para cualquier enumeración, no solo para caracteres, y son la manera correcta de hacer aritmética sobre enumerados sin asumir representaciones.

Para meter una comilla doble dentro de un literal de cadena se duplica: "Dijo ""hola"" y se fue".

El tipo String

String es un arreglo no restringido de Character, así que hereda todo lo de la sección anterior: rebanadas, atributos, agregados, verificación de índice.

with Ada.Text_IO;

procedure Cadenas_Fijas is

   --  Los bordes se deducen del literal: 1 .. 12.
   Saludo : constant String := "Hola, mundo!";

   --  Bordes explícitos: el literal debe medir exactamente 5.
   Corta : String (1 .. 5) := "abcde";

   --  Concatenación con &.
   Nombre   : constant String := "Ada";
   Apellido : constant String := "Lovelace";
   Completo : constant String := Nombre & " " & Apellido;

   --  Un salto de línea dentro de una constante.
   Parrafo : constant String :=
     "Primera línea" & ASCII.LF & "Segunda línea";

begin
   Ada.Text_IO.Put_Line (Saludo);
   Ada.Text_IO.Put_Line ("Largo:" & Integer'Image (Saludo'Length));
   Ada.Text_IO.Put_Line ("Primeros cuatro: " & Saludo (1 .. 4));
   Ada.Text_IO.Put_Line ("Último carácter: " & Saludo (Saludo'Last));
   Ada.Text_IO.Put_Line (Completo);
   Ada.Text_IO.Put_Line (Parrafo);

   --  Asignación completa: los largos deben coincidir.
   Corta := "vwxyz";
   Ada.Text_IO.Put_Line (Corta);

   --  Comparación lexicográfica.
   Ada.Text_IO.Put_Line
     ("¿""abc"" < ""abd""? " & Boolean'Image ("abc" < "abd"));
end Cadenas_Fijas;

La restricción clave del tipo String: el largo se fija cuando el objeto se crea y no cambia. Corta := "abc" con Corta : String (1 .. 5) levanta Constraint_Error en ejecución, porque tres no es cinco. Esta rigidez es lo que permite que String no necesite memoria dinámica: cabe en la pila y su tamaño es conocido.

Operaciones sobre cadenas fijas

El paquete Ada.Strings.Fixed agrega las operaciones que faltan sin salirse del tipo String.

with Ada.Text_IO;
with Ada.Strings;
with Ada.Strings.Fixed;
with Ada.Characters.Handling;

procedure Operaciones_Cadenas is

   use Ada.Strings.Fixed;

   Frase : constant String := "  el kernel administra recursos  ";

begin
   --  Quitar espacios.
   Ada.Text_IO.Put_Line
     ("[" & Trim (Frase, Ada.Strings.Both) & "]");
   Ada.Text_IO.Put_Line
     ("[" & Trim (Frase, Ada.Strings.Left) & "]");

   --  Buscar una subcadena: devuelve el índice, o 0 si no está.
   Ada.Text_IO.Put_Line
     ("Índice de 'kernel':" & Integer'Image (Index (Frase, "kernel")));
   Ada.Text_IO.Put_Line
     ("Índice de 'hilo':" & Integer'Image (Index (Frase, "hilo")));

   --  Contar apariciones.
   Ada.Text_IO.Put_Line
     ("Veces que aparece 'r':" & Integer'Image (Count (Frase, "r")));

   --  Rellenar a un ancho fijo.
   Ada.Text_IO.Put_Line ("[" & Head ("nombre", 15) & "]");
   Ada.Text_IO.Put_Line ("[" & Tail ("42", 8, '0') & "]");

   --  Repetir un carácter o una cadena.
   Ada.Text_IO.Put_Line (40 * '-');
   Ada.Text_IO.Put_Line (3 * "ab");

   --  Mayúsculas y minúsculas.
   Ada.Text_IO.Put_Line
     (Ada.Characters.Handling.To_Upper ("sistemas operativos"));
   Ada.Text_IO.Put_Line
     (Ada.Characters.Handling.To_Lower ("SISTEMAS OPERATIVOS"));

   --  Clasificación de caracteres.
   Ada.Text_IO.Put_Line
     ("¿'7' es dígito? "
      & Boolean'Image (Ada.Characters.Handling.Is_Digit ('7')));
end Operaciones_Cadenas;

Index devuelve 0 cuando no encuentra el patrón, y ese cero está fuera del rango Positive de índices válidos, así que no se puede confundir con una posición real.

Cadenas acotadas

Una cadena acotada tiene un largo máximo fijado en la instanciación, pero un largo actual que varía. Vive donde vive el objeto, sin montón.

with Ada.Text_IO;
with Ada.Strings.Bounded;

procedure Cadenas_Acotadas is

   package Cadena_80 is new Ada.Strings.Bounded.Generic_Bounded_Length
     (Max => 80);

   use Cadena_80;

   Linea : Bounded_String := To_Bounded_String ("inicio");

begin
   Ada.Text_IO.Put_Line (To_String (Linea));
   Ada.Text_IO.Put_Line ("Largo:" & Integer'Image (Length (Linea)));
   Ada.Text_IO.Put_Line ("Máximo:" & Integer'Image (Max_Length));

   Append (Linea, " y continuación");
   Ada.Text_IO.Put_Line (To_String (Linea));
   Ada.Text_IO.Put_Line ("Largo:" & Integer'Image (Length (Linea)));
end Cadenas_Acotadas;

Si intentas guardar más caracteres que Max, el comportamiento lo decide un parámetro de la operación: por defecto se levanta Ada.Strings.Length_Error, y también se puede pedir que trunque por la izquierda o por la derecha. Esta es la estructura de texto que usan los sistemas críticos donde el montón está prohibido pero se necesita largo variable: el peor caso de memoria es conocido y acotado.

Cadenas no acotadas

Ada.Strings.Unbounded da el equivalente a un std::string de C++ o a un String de Java: largo arbitrario, memoria del montón, gestión automática.

with Ada.Text_IO;
with Ada.Strings.Unbounded;

procedure Cadenas_Dinamicas is

   use Ada.Strings.Unbounded;

   Mensaje : Unbounded_String := To_Unbounded_String ("Hola");
   Buffer  : Unbounded_String := Null_Unbounded_String;

begin
   --  Concatenar produce una cadena nueva del tamaño necesario.
   Mensaje := Mensaje & " mundo";
   Mensaje := Mensaje & ", desde Ada";

   Ada.Text_IO.Put_Line (To_String (Mensaje));
   Ada.Text_IO.Put_Line ("Largo:" & Integer'Image (Length (Mensaje)));

   --  Append modifica en el lugar.
   Append (Buffer, "línea 1");
   Append (Buffer, ASCII.LF);
   Append (Buffer, "línea 2");
   Ada.Text_IO.Put_Line (To_String (Buffer));

   --  Acceso a un carácter y a una porción.
   Ada.Text_IO.Put_Line ("Primer carácter: " & Element (Mensaje, 1));
   Ada.Text_IO.Put_Line ("Porción 1..4: " & Slice (Mensaje, 1, 4));

   --  Búsqueda.
   Ada.Text_IO.Put_Line
     ("Índice de 'Ada':" & Integer'Image (Index (Mensaje, "Ada")));

   --  Comparación e igualdad funcionan directamente.
   Ada.Text_IO.Put_Line
     ("¿Vacío el buffer? "
      & Boolean'Image (Buffer = Null_Unbounded_String));
end Cadenas_Dinamicas;

Unbounded_String es un tipo controlado: el compilador inserta llamadas automáticas de inicialización, ajuste y finalización, de modo que la memoria se libera cuando el objeto sale de alcance. No hay free manual y no hay fugas por olvido. El precio es que cada concatenación puede implicar una asignación en el montón, y el montón, a nivel de sistema operativo, se apoya en llamadas que amplían el espacio de direcciones del proceso.

flowchart TD
    subgraph fijas["String"]
      A1["Largo fijo al crear el objeto"]
      A2["Vive donde vive el objeto: pila o dato estático"]
      A3["Sin montón, sin sorpresas de latencia"]
      A4["Asignar un largo distinto: Constraint_Error"]
    end
    subgraph acotadas["Bounded_String"]
      B1["Largo variable hasta un máximo fijado"]
      B2["Reserva el máximo por adelantado"]
      B3["Sin montón: memoria acotada y predecible"]
      B4["Pasarse del máximo: Length_Error o truncado"]
    end
    subgraph dinamicas["Unbounded_String"]
      C1["Largo totalmente variable"]
      C2["Datos en el montón del proceso"]
      C3["Tipo controlado: libera al salir de alcance"]
      C4["Costo: asignaciones y latencia no acotada"]
    end
    D["¿Qué necesito?"] --> E{"¿El largo cambia?"}
    E -->|"no"| fijas
    E -->|"sí"| F{"¿Conozco una cota superior<br/>y necesito latencia predecible?"}
    F -->|"sí"| acotadas
    F -->|"no"| dinamicas
CriterioStringBounded_StringUnbounded_String
Largofijo por objetovariable hasta Maxlibre
Memoriapila o estáticapila o estáticamontón
Costo de concatenarcopia a un objeto nuevocopia dentro del búferposible asignación dinámica
Apto para tiempo real duronormalmente no
Conversión al otroya es StringTo_String / To_Bounded_StringTo_String / To_Unbounded_String
Error típicoConstraint_Error por largo distintoLength_Error al pasarseagotamiento del montón

Unicode y UTF-8

El tipo String tiene elementos de ocho bits. Un carácter como ñ o é en UTF-8 ocupa dos bytes, y un emoji cuatro. Eso significa que un String que contiene texto UTF-8 tiene un 'Length que cuenta bytes, no caracteres percibidos. Es exactamente la misma situación que en C, y hay que tenerla presente.

Ada ofrece dos caminos, y conviene no mezclarlos.

El primero es trabajar con String como una secuencia de bytes UTF-8 y limitarse a leer, concatenar y escribir. Para que GNAT interprete el archivo fuente como UTF-8 y acepte literales con acentos, hay que compilar con la opción -gnatW8. En un proyecto de Alire eso se declara en el archivo .gpr:

project Mi_Programa is

   for Source_Dirs use ("src");
   for Object_Dir use "obj";
   for Exec_Dir use "bin";
   for Main use ("mi_programa.adb");

   package Compiler is
      for Switches ("Ada") use ("-gnatW8", "-gnat2022", "-g", "-gnatwa");
   end Compiler;

end Mi_Programa;

El segundo camino es trabajar con Wide_Wide_String, donde cada elemento es un punto de código completo, y convertir a bytes solo en el borde de entrada y salida. El paquete estándar Ada.Strings.UTF_Encoding.Wide_Wide_Strings hace esa conversión.

with Ada.Text_IO;
with Ada.Strings.UTF_Encoding;
with Ada.Strings.UTF_Encoding.Wide_Wide_Strings;

procedure Texto_Unicode is

   use Ada.Strings.UTF_Encoding;
   use Ada.Strings.UTF_Encoding.Wide_Wide_Strings;

   --  Cada elemento es un punto de código, no un byte.
   Palabra : constant Wide_Wide_String := "Ñandú";

   --  Codificada a bytes UTF-8, lista para escribir por la salida estándar.
   Bytes : constant UTF_8_String := Encode (Palabra);

begin
   Ada.Text_IO.Put_Line (Bytes);
   Ada.Text_IO.Put_Line
     ("Puntos de código:" & Integer'Image (Palabra'Length));
   Ada.Text_IO.Put_Line
     ("Bytes en UTF-8:" & Integer'Image (Bytes'Length));

   --  De vuelta: bytes a puntos de código.
   declare
      Recuperada : constant Wide_Wide_String := Decode (Bytes);
   begin
      Ada.Text_IO.Put_Line
        ("¿Ida y vuelta correcta? "
         & Boolean'Image (Recuperada = Palabra));
   end;
end Texto_Unicode;

La salida muestra la diferencia con claridad: cinco puntos de código, siete bytes, porque Ñ y ú ocupan dos bytes cada uno. Compila esto con -gnatW8 para que el literal "Ñandú" del archivo fuente se lea correctamente.

Una recomendación práctica que vale más allá de Ada: usa solo ASCII en los identificadores del código. Ada permite acentos en nombres de variables, pero un identificador que no se puede escribir en cualquier teclado es un obstáculo real en un equipo distribuido. Los acentos van en los textos que ve el usuario, no en los nombres de las cosas.

Argumentos de línea de comandos

Hasta acá los programas hacían siempre lo mismo. Un programa útil recibe información de afuera, y la vía más directa es la línea de comandos.

Cómo llegan los argumentos desde el sistema operativo

Esto no es magia de Ada. Cuando escribes ./adivina 1 50 en el shell, ocurre una secuencia concreta que ya conocemos de los capítulos sobre procesos.

sequenceDiagram
    participant U as Usuario
    participant SH as Shell
    participant K as Kernel
    participant RT as Runtime de Ada
    participant P as Programa Ada

    U->>SH: ./adivina 1 50
    SH->>SH: divide la línea en palabras
    SH->>SH: expande comodines y variables
    SH->>K: fork: crea un proceso hijo
    K-->>SH: identificador del hijo
    SH->>K: execve ruta, vector argv, vector envp
    K->>K: reemplaza la imagen del proceso
    K->>K: copia argv y envp al stack inicial
    K->>RT: entrega el control al punto de entrada
    RT->>RT: guarda punteros a argc, argv, envp
    RT->>P: llama al procedimiento principal
    P->>RT: Ada.Command_Line.Argument_Count
    RT-->>P: 2
    P->>RT: Ada.Command_Line.Argument(1)
    RT-->>P: "1"
    P->>K: exit con código de salida
    K-->>SH: estado de terminación
    SH->>U: muestra el prompt de nuevo

Dos consecuencias prácticas se desprenden del diagrama. La primera es que el shell ya hizo su trabajo antes de que tu programa exista: separó por espacios, expandió *.txt en nombres de archivo y sustituyó $HOME. Tu programa recibe el resultado, no la línea original. La segunda es que el nombre del ejecutable viaja en la posición cero del vector, separado de los argumentos propiamente tales, y por eso en Ada se consulta con una función distinta.

El paquete Ada.Command_Line

ElementoTipoQué hace
Argument_Countfunción que devuelve Naturalcantidad de argumentos, sin contar el nombre del programa
Argument (N)función que devuelve Stringel argumento en la posición N, con N de 1 a Argument_Count
Command_Namefunción que devuelve Stringel nombre con que se invocó el programa
Set_Exit_Status (C)procedimientofija el código que el proceso devolverá al terminar
Successconstantecódigo de salida que indica éxito
Failureconstantecódigo de salida que indica error
with Ada.Text_IO;
with Ada.Command_Line;

procedure Eco is
   use Ada.Text_IO;
   use Ada.Command_Line;
begin
   Put_Line ("Programa: " & Command_Name);
   Put_Line ("Cantidad de argumentos:" & Natural'Image (Argument_Count));

   if Argument_Count = 0 then
      Put_Line ("No recibí argumentos.");
      return;
   end if;

   Put_Line ("--- en orden ---");
   for N in 1 .. Argument_Count loop
      Put_Line (Natural'Image (N) & ": " & Argument (N));
   end loop;

   Put_Line ("--- en orden inverso ---");
   for N in reverse 1 .. Argument_Count loop
      Put_Line (Natural'Image (N) & ": " & Argument (N));
   end loop;
end Eco;

Compilado y ejecutado con gnatmake eco.adb y ./eco Yo Como Pan, la salida lista los tres argumentos hacia adelante y hacia atrás. Con Alire la invocación es alr run --args="Yo Como Pan", porque alr necesita distinguir sus propios argumentos de los del programa.

La palabra reservada reverse en un bucle for recorre el rango de mayor a menor sin cambiar el rango en sí. Es más claro y menos propenso a errores que calcular índices descendentes a mano.

Un detalle importante sobre Argument (N): devuelve un String cuyo largo depende del argumento concreto. Por eso casi siempre se lo asigna a una constante en un bloque declare, donde los bordes se deducen automáticamente.

Convertir y validar argumentos

Los argumentos siempre llegan como texto. Convertirlos a números es donde aparecen los errores, porque el usuario puede escribir cualquier cosa.

with Ada.Text_IO;
with Ada.Command_Line;

procedure Sumar_Args is

   use Ada.Text_IO;
   use Ada.Command_Line;

   Total : Integer := 0;

begin
   if Argument_Count = 0 then
      Put_Line ("Uso: " & Command_Name & " numero [numero ...]");
      Set_Exit_Status (Failure);
      return;
   end if;

   for N in 1 .. Argument_Count loop
      declare
         Texto : constant String := Argument (N);
      begin
         Total := Total + Integer'Value (Texto);
      exception
         when Constraint_Error =>
            Put_Line ("El argumento " & Natural'Image (N)
                      & " no es un entero válido: " & Texto);
            Set_Exit_Status (Failure);
            return;
      end;
   end loop;

   Put_Line ("Suma:" & Integer'Image (Total));
   Set_Exit_Status (Success);
end Sumar_Args;

El atributo 'Value es el inverso de 'Image: convierte texto a valor. Tolera espacios adelante y atrás, y levanta Constraint_Error cuando el texto no representa un valor válido del tipo, ya sea porque tiene letras o porque el número está fuera de rango. Ese comportamiento hace que la validación sea trivial: intentas la conversión y capturas la excepción.

Set_Exit_Status es lo que hace que el programa se comporte como un ciudadano correcto del sistema operativo. Prueba esto en el shell después de compilar:

gnatmake sumar_args.adb
./sumar_args 10 20 30 && echo "todo bien"
./sumar_args 10 hola 30 || echo "falló como corresponde"
echo $?

El operador && solo continúa si el código de salida es cero, y || solo si no lo es. Un programa que no fija su estado de salida rompe los guiones que lo usan.

Un analizador de opciones a mano

Los programas reales aceptan opciones con nombre, no solo posiciones. GNAT ofrece el paquete GNAT.Command_Line con una función Getopt para eso, pero escribir el recorrido a mano una vez ayuda a entender qué hace.

with Ada.Text_IO;
with Ada.Command_Line;
with Ada.Strings.Unbounded;

procedure Opciones is

   use Ada.Text_IO;
   use Ada.Command_Line;
   use Ada.Strings.Unbounded;

   Verboso : Boolean := False;
   Archivo : Unbounded_String := Null_Unbounded_String;
   Repetir : Positive := 1;
   Error   : Boolean := False;
   N       : Natural := 1;

begin
   while N <= Argument_Count and then not Error loop
      declare
         Opcion  : constant String := Argument (N);
         Hay_Mas : constant Boolean := N < Argument_Count;
      begin
         if Opcion = "-v" or else Opcion = "--verbose" then
            Verboso := True;

         elsif Opcion = "-f" and then Hay_Mas then
            N := N + 1;
            Archivo := To_Unbounded_String (Argument (N));

         elsif Opcion = "-n" and then Hay_Mas then
            N := N + 1;
            begin
               Repetir := Positive'Value (Argument (N));
            exception
               when Constraint_Error =>
                  Put_Line ("-n espera un entero positivo.");
                  Error := True;
            end;

         else
            Put_Line ("Opción inválida o sin valor: " & Opcion);
            Error := True;
         end if;
      end;

      N := N + 1;
   end loop;

   if Error then
      Put_Line ("Uso: " & Command_Name & " [-v] [-f ARCHIVO] [-n VECES]");
      Set_Exit_Status (Failure);
      return;
   end if;

   if Verboso then
      Put_Line ("Archivo: " & To_String (Archivo));
   end if;

   for I in 1 .. Repetir loop
      Put_Line ("Ejecución" & Integer'Image (I));
   end loop;

   Set_Exit_Status (Success);
end Opciones;

El bucle usa while en lugar de for porque el índice avanza de a uno o de a dos según si la opción consume un valor. Es justamente el caso donde for no sirve: en Ada un for sobre un rango discreto avanza exactamente un paso por iteración y no se puede modificar la variable de control desde adentro. Esa variable es constante dentro del cuerpo del bucle, lo que elimina una clase entera de errores de recorrido.

Variables de entorno

Los argumentos no son la única información que el proceso hereda. El paquete Ada.Environment_Variables da acceso al otro vector que el kernel copió en el arranque: Exists ("HOME") responde si la variable existe, Value ("HOME") la devuelve como String y levanta Constraint_Error si no está, Value ("SHELL", "desconocido") permite dar un valor por defecto, Set la modifica e Iterate recorre todas. Vale recordar lo que ya sabemos de procesos: cambiar el entorno dentro del programa no afecta al shell que lo lanzó, porque el hijo recibió una copia.

Leer datos del usuario

Para el juego necesitamos leer desde la entrada estándar. Ada ofrece dos enfoques y la diferencia entre ellos es la fuente de errores más común de quien empieza.

Get_Line frente a Get

Ada.Text_IO.Get_Line lee hasta el fin de línea y consume el salto de línea. Ada.Integer_Text_IO.Get lee un número y deja el salto de línea sin consumir en el búfer de entrada, esperando a que alguien lo lea.

with Ada.Text_IO;
with Ada.Integer_Text_IO;

procedure Calculadora is
   Primero, Segundo : Integer;
begin
   Ada.Text_IO.Put ("Ingresa dos números separados por espacio o salto: ");
   Ada.Integer_Text_IO.Get (Primero);
   Ada.Integer_Text_IO.Get (Segundo);
   Ada.Text_IO.Skip_Line;   --  consume el resto de la línea

   Ada.Text_IO.Put ("La suma es:");
   Ada.Integer_Text_IO.Put (Primero + Segundo);
   Ada.Text_IO.New_Line;
end Calculadora;

Sin ese Skip_Line, si después haces un Get_Line obtendrás una cadena vacía: el salto pendiente la termina de inmediato. Es el bug clásico del “el programa no espera mi respuesta”.

OperaciónQué leeDeja el salto pendienteExcepción típica
Ada.Text_IO.Get_Line (función)una línea completa como StringnoEnd_Error al final del flujo
Ada.Text_IO.Get_Line (S, L)hasta llenar S, con L caracteres leídosno si cupoEnd_Error
Ada.Text_IO.Get (C)un carácterEnd_Error
Ada.Integer_Text_IO.Get (N)un entero, salteando espaciosData_Error si no es número
Ada.Float_Text_IO.Get (F)un realData_Error
Ada.Text_IO.Skip_Linedescarta hasta el próximo saltonoEnd_Error

Lectura robusta

La forma que no se rompe es leer siempre una línea completa con Get_Line y convertir con 'Value dentro de un bloque con manejador de excepciones. Así el búfer queda siempre limpio y la entrada inválida se maneja sin caídas. Este procedimiento es el que vamos a reutilizar en el juego.

   type Estado_Lectura is (Valido, No_Numerico, Fin_De_Entrada);

   procedure Leer_Entero
     (Mensaje : String;
      Valor   : out Integer;
      Estado  : out Estado_Lectura)
   is
   begin
      Ada.Text_IO.Put (Mensaje);
      declare
         Linea : constant String := Ada.Text_IO.Get_Line;
      begin
         Valor  := Integer'Value (Linea);
         Estado := Valido;
      end;
   exception
      when Constraint_Error =>
         Valor  := 0;
         Estado := No_Numerico;
      when Ada.Text_IO.End_Error =>
         Valor  := 0;
         Estado := Fin_De_Entrada;
   end Leer_Entero;

El bloque declare ... begin ... end interno existe porque Linea es una constante cuyo largo se conoce recién al ejecutar Get_Line. Las excepciones que levanta ese bloque suben al manejador del procedimiento, que las clasifica en el tipo enumerado Estado_Lectura. Devolver un enumerado en lugar de un booleano permite distinguir “escribió basura” de “cerró la entrada”, que son situaciones distintas y merecen respuestas distintas. Si el usuario presiona Ctrl+D en una terminal Unix, la lectura devuelve fin de archivo y Get_Line levanta End_Error: el mismo mecanismo que hace terminar a cat.

El juego de adivinar el número

Ahora sí. Juntamos todas las piezas del capítulo en un solo programa.

Números aleatorios en Ada

Ada genera números pseudoaleatorios con paquetes genéricos que se instancian sobre el tipo que quieres sortear. Para enteros y enumeraciones se usa Ada.Numerics.Discrete_Random; para reales, Ada.Numerics.Float_Random.

with Ada.Text_IO;
with Ada.Numerics.Discrete_Random;

procedure Dado is

   subtype Cara is Integer range 1 .. 6;

   package Sorteo is new Ada.Numerics.Discrete_Random (Cara);

   Generador : Sorteo.Generator;

begin
   --  Sin argumento: la semilla depende del momento de ejecución.
   Sorteo.Reset (Generador);
   for Tirada in 1 .. 10 loop
      Ada.Text_IO.Put (Integer'Image (Sorteo.Random (Generador)));
   end loop;
   Ada.Text_IO.New_Line;

   --  Con semilla fija: la secuencia se repite igual en cada ejecución.
   Sorteo.Reset (Generador, 42);
   for Tirada in 1 .. 10 loop
      Ada.Text_IO.Put (Integer'Image (Sorteo.Random (Generador)));
   end loop;
   Ada.Text_IO.New_Line;
end Dado;

Tres cosas que conviene grabar. Primero, el generador se instancia sobre un subtipo discreto, así que el resultado ya viene dentro del rango correcto: no hace falta el mod que en C introduce sesgo. Segundo, Reset sin segundo argumento usa una fuente que varía con el tiempo; sin ese Reset la secuencia sería idéntica en cada ejecución. Tercero, Reset con una semilla entera hace que la secuencia sea reproducible, lo que es indispensable para escribir pruebas automáticas de un programa que sortea.

Estos generadores no son criptográficamente seguros. Para claves o tokens hay que ir a la fuente de entropía del sistema operativo, no a un generador pseudoaleatorio de biblioteca.

La máquina de estados del juego

stateDiagram-v2
    [*] --> Configurando
    Configurando --> Sorteando: argumentos válidos
    Configurando --> Terminado_Error: argumentos inválidos
    Sorteando --> Esperando: número secreto listo
    Esperando --> Evaluando: el usuario escribió una línea
    Esperando --> Terminado_EOF: fin de entrada, Ctrl+D
    Evaluando --> Avisando_Invalido: la línea no es un entero
    Evaluando --> Avisando_Rango: el entero está fuera del rango
    Evaluando --> Avisando_Menor: intento menor que el secreto
    Evaluando --> Avisando_Mayor: intento mayor que el secreto
    Evaluando --> Ganado: intento igual al secreto
    Avisando_Invalido --> Esperando
    Avisando_Rango --> Esperando
    Avisando_Menor --> Verificando_Limite
    Avisando_Mayor --> Verificando_Limite
    Verificando_Limite --> Esperando: quedan intentos
    Verificando_Limite --> Perdido: se agotaron los intentos
    Ganado --> Resumen
    Perdido --> Resumen
    Resumen --> [*]
    Terminado_EOF --> [*]
    Terminado_Error --> [*]

Los estados que no son “esperar” y “comparar” son justamente los que separan un ejercicio de clase de un programa que aguanta uso real: configuración inválida, fin de entrada, fuera de rango, límite de intentos alcanzado.

Versión 2: el juego completo

Esta versión usa arreglos y contenedores para el historial, cadenas para armar mensajes, argumentos de línea de comandos para configurar el rango y la semilla, lectura robusta, límite de intentos y código de salida.

with Ada.Text_IO;
with Ada.Command_Line;
with Ada.Numerics.Discrete_Random;
with Ada.Containers.Vectors;

procedure Adivina is

   use Ada.Text_IO;

   type Estado_Lectura is (Valido, No_Numerico, Fin_De_Entrada);

   package Historiales is new Ada.Containers.Vectors
     (Index_Type   => Positive,
      Element_Type => Integer);

   Minimo       : Integer := 1;
   Maximo       : Integer := 100;
   Semilla      : Integer := 0;
   Usar_Semilla : Boolean := False;

   procedure Mostrar_Uso is
   begin
      Put_Line ("Uso: " & Ada.Command_Line.Command_Name
                & " [minimo maximo [semilla]]");
      Put_Line ("  sin argumentos: juega entre 1 y 100");
      Put_Line ("  con dos:        define el rango");
      Put_Line ("  con tres:       además fija la semilla, partida reproducible");
   end Mostrar_Uso;

   procedure Leer_Entero
     (Mensaje : String;
      Valor   : out Integer;
      Estado  : out Estado_Lectura)
   is
   begin
      Put (Mensaje);
      declare
         Linea : constant String := Get_Line;
      begin
         Valor  := Integer'Value (Linea);
         Estado := Valido;
      end;
   exception
      when Constraint_Error =>
         Valor  := 0;
         Estado := No_Numerico;
      when End_Error =>
         Valor  := 0;
         Estado := Fin_De_Entrada;
   end Leer_Entero;

   function Intentos_Permitidos (Ancho : Positive) return Positive is
      Resultado : Positive := 1;
      Restante  : Positive := Ancho;
   begin
      --  Una búsqueda binaria necesita techo de log2 del ancho del rango.
      while Restante > 1 loop
         Restante  := (Restante + 1) / 2;
         Resultado := Resultado + 1;
      end loop;
      return Resultado;
   end Intentos_Permitidos;

begin
   --  ---------- Configuración desde la línea de comandos ----------
   case Ada.Command_Line.Argument_Count is
      when 0 =>
         null;

      when 2 | 3 =>
         begin
            Minimo := Integer'Value (Ada.Command_Line.Argument (1));
            Maximo := Integer'Value (Ada.Command_Line.Argument (2));
            if Ada.Command_Line.Argument_Count = 3 then
               Semilla      := Integer'Value (Ada.Command_Line.Argument (3));
               Usar_Semilla := True;
            end if;
         exception
            when Constraint_Error =>
               Put_Line ("Los argumentos deben ser números enteros.");
               Mostrar_Uso;
               Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure);
               return;
         end;

      when others =>
         Put_Line ("Cantidad de argumentos incorrecta.");
         Mostrar_Uso;
         Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure);
         return;
   end case;

   if Minimo >= Maximo then
      Put_Line ("El mínimo debe ser menor que el máximo.");
      Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure);
      return;
   end if;

   --  ---------- Partida ----------
   declare
      --  El subtipo se construye con valores conocidos recién ahora.
      subtype Rango is Integer range Minimo .. Maximo;

      package Sorteo is new Ada.Numerics.Discrete_Random (Rango);

      Generador : Sorteo.Generator;
      Secreto   : Rango;
      Historial : Historiales.Vector;
      Intento   : Integer;
      Estado    : Estado_Lectura;
      Acerto    : Boolean := False;
      Maximo_Intentos : constant Positive :=
        Intentos_Permitidos (Maximo - Minimo + 1);

   begin
      if Usar_Semilla then
         Sorteo.Reset (Generador, Semilla);
      else
         Sorteo.Reset (Generador);
      end if;

      Secreto := Sorteo.Random (Generador);

      Put_Line ("Pensé un número entre" & Integer'Image (Minimo)
                & " y" & Integer'Image (Maximo) & ".");
      Put_Line ("Tienes" & Positive'Image (Maximo_Intentos)
                & " intentos. Con búsqueda binaria alcanza justo.");
      New_Line;

      while not Acerto
        and then Integer (Historial.Length) < Maximo_Intentos
      loop
         Leer_Entero ("Intento "
                      & Integer'Image (Integer (Historial.Length) + 1)
                      & ": ", Intento, Estado);

         case Estado is
            when Fin_De_Entrada =>
               New_Line;
               Put_Line ("Entrada cerrada. El número era"
                         & Integer'Image (Secreto) & ".");
               Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure);
               return;

            when No_Numerico =>
               Put_Line ("  Eso no es un número entero. No cuenta como intento.");

            when Valido =>
               if Intento < Minimo or else Intento > Maximo then
                  Put_Line ("  Está fuera del rango. No cuenta como intento.");
               else
                  Historial.Append (Intento);

                  if Intento < Secreto then
                     Put_Line ("  Muy pequeño.");
                  elsif Intento > Secreto then
                     Put_Line ("  Muy grande.");
                  else
                     Acerto := True;
                  end if;
               end if;
         end case;
      end loop;

      New_Line;

      if Acerto then
         Put_Line ("Correcto. Lo lograste en"
                   & Integer'Image (Integer (Historial.Length))
                   & " intentos.");
      else
         Put_Line ("Se acabaron los intentos. El número era"
                   & Integer'Image (Secreto) & ".");
      end if;

      Put ("Tu recorrido:");
      for Valor of Historial loop
         Put (Integer'Image (Valor));
      end loop;
      New_Line;

      --  Distancia del último intento al objetivo.
      if not Historial.Is_Empty then
         Put_Line
           ("Te quedaste a"
            & Integer'Image (abs (Historial.Last_Element - Secreto))
            & " de distancia en el último intento.");
      end if;

      if Acerto then
         Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Success);
      else
         Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure);
      end if;
   end;
end Adivina;

Vale la pena detenerse en dos decisiones de este código.

La primera es el bloque declare que contiene la partida. subtype Rango is Integer range Minimo .. Maximo usa variables, no constantes literales, y eso es perfectamente legal: Ada permite subtipos con bordes calculados en ejecución, y esos bordes se fijan cuando se elabora la declaración. La instanciación package Sorteo is new ... ocurre en ese mismo bloque. Así el generador queda restringido al rango que pidió el usuario, sin trucos aritméticos.

La segunda es Intentos_Permitidos. Calcula el techo del logaritmo base dos del ancho del rango, que es la cantidad de preguntas que necesita una búsqueda binaria en el peor caso. Está escrito con aritmética entera, sin recurrir a la biblioteca de reales, porque mezclar flotantes solo para un logaritmo introduce problemas de redondeo en los bordes. Para un rango de 1 a 100 devuelve 7, que es el número correcto.

Para probarlo:

gnatmake adivina.adb

./adivina                 # rango por defecto 1..100
./adivina 1 1000          # rango más ancho, más intentos permitidos
./adivina 1 100 42        # partida reproducible con semilla 42
./adivina 100 1           # error de configuración
./adivina uno dos         # error de conversión
echo $?                   # inspecciona el código de salida

Con Alire, el equivalente es alr run --args="1 100 42".

La estrategia ganadora

El límite de intentos no es arbitrario: coincide exactamente con lo que necesita una búsqueda binaria. La estrategia consiste en partir siempre por el medio del intervalo que todavía es posible, y ajustar el borde según la pista recibida. Con un rango de cien números, siete intentos bastan siempre; con mil, diez.

Una advertencia sobre el cálculo del punto medio: escribe Medio := Bajo + (Alto - Bajo) / 2 en lugar de (Bajo + Alto) / 2. La segunda forma desborda cuando ambos extremos son grandes, y es el mismo defecto que estuvo latente durante años en implementaciones de búsqueda binaria de bibliotecas muy usadas. En Ada un desbordamiento entero levanta Constraint_Error en vez de dar un resultado silenciosamente equivocado, pero la forma correcta sigue siendo la correcta.

Errores comunes y cómo resolverlos

ErrorCausaSíntomaSolución
Constraint_Error: index check failedíndice fuera del rango del arregloel programa termina con traza en ejecuciónrecorrer con for I in A'Range en lugar de rangos escritos a mano
Constraint_Error: length check failed al asignar una cadenael largo del literal no coincide con el del objeto Stringfalla en tiempo de ejecución o de compilaciónusar constant String := "..." para deducir bordes, o Unbounded_String si el largo cambia
El programa no espera la respuesta del usuarioun Get numérico dejó el salto de línea sin consumirGet_Line devuelve una cadena vacía de inmediatoleer siempre con Get_Line y convertir con 'Value, o llamar a Skip_Line tras cada Get
El programa se cae si el usuario escribe letrasInteger'Value levanta Constraint_Error sin manejadortraza de excepción no capturadaenvolver la conversión en un bloque con exception when Constraint_Error
El programa se cuelga o repite al presionar Ctrl+Dno se maneja End_Error en el bucle de lecturabucle infinito imprimiendo el mensaje de errorcapturar Ada.Text_IO.End_Error y salir del bucle
Argument (0) fallael índice de Argument empieza en 1Constraint_Error en el primer accesousar Command_Name para el nombre del programa
Los argumentos llegan como una sola cadena con Alirefalta el formato correcto de la opciónel programa cree que recibió un solo argumentousar alr run --args="a b c" con comillas
El sorteo devuelve siempre la misma secuenciafalta llamar a Reset sobre el generadorel juego es idéntico en cada ejecuciónllamar a Sorteo.Reset (Generador) antes del primer Random
Los acentos salen como caracteres rarosGNAT no está interpretando el fuente como UTF-8mojibake en la salidacompilar con -gnatW8 y declararlo en el paquete Compiler del .gpr
'Length de una cadena con acentos da más de lo esperadoString cuenta bytes, y UTF-8 usa varios por caráctercálculos de ancho de columna equivocadosusar Wide_Wide_String con Ada.Strings.UTF_Encoding cuando importan los puntos de código
El compilador rechaza Total / Cantidadmezcla de Integer y Float sin conversiónerror de tipos en compilaciónconvertir explícitamente: Float (Total) / Float (Cantidad)
V.Length no se puede sumar a un IntegerLength devuelve Ada.Containers.Count_Typeerror de tipos en compilaciónconvertir con Integer (V.Length)
Un cursor deja de funcionar tras insertarla operación estructural invalidó el cursorexcepción al usarlovolver a obtener el cursor tras la modificación, o iterar con for E of
El shell no distingue éxito de errorel programa nunca llamó a Set_Exit_Statuslos encadenamientos con && y `
Modificar la variable de un for da error de compilaciónen Ada la variable de control es constante dentro del buclerechazo del compiladorusar while cuando el avance no es de uno en uno
Una rebanada tiene índices inesperadoslas rebanadas conservan los índices del originalCola'First no vale 1asignar la rebanada a una constante nueva si se quieren índices desde 1

Ejercicios propuestos

1. Estadísticas sobre un arreglo no restringido. Escribe una función Mediana que reciba un array (Positive range <>) of Integer y devuelva la mediana. Tendrás que ordenar una copia sin modificar el original. Implementa el ordenamiento tú mismo con selección o inserción, y verifica que funciona con arreglos de largo par e impar.

2. Índices con enumeración. Declara un tipo Mes con los doce meses y un arreglo array (Mes) of Natural con los días de cada uno. Escribe un procedimiento que reciba un año y devuelva ese arreglo con febrero corregido según si es bisiesto. Recorre el resultado imprimiendo cada mes con su cantidad de días.

3. Rebanadas sin copiar. Escribe una función que reciba un String y devuelva la posición de la primera palabra de más de cinco caracteres, sin usar Ada.Strings.Fixed. Trabaja solo con rebanadas y atributos. Después reescríbela usando Index y compara la cantidad de líneas.

4. Contar palabras. Escribe un programa que lea líneas de la entrada estándar hasta el fin de archivo, separe cada línea en palabras y acumule las frecuencias en un Ordered_Map con clave Unbounded_String. Al final imprime las palabras ordenadas por frecuencia descendente. Pruébalo con cat archivo.txt | ./contar.

5. Vector contra lista. Implementa el mismo programa dos veces: una insertando cien mil elementos al principio de un Vector y otra al principio de una Doubly_Linked_List. Mide ambos con time y explica la diferencia en términos de lo que cada estructura tiene que hacer en memoria.

6. Las tres cadenas. Escribe tres versiones de un programa que concatene mil fragmentos de texto: una con String y &, otra con Bounded_String y Append, y otra con Unbounded_String y Append. Compara tiempos y explica cuál de las tres hace más trabajo de asignación de memoria.

7. Ancho real de una cadena. Escribe una función que reciba un String con bytes UTF-8 y devuelva la cantidad de puntos de código, sin usar Ada.Strings.UTF_Encoding. Tendrás que inspeccionar los bits de cada byte para reconocer los bytes de continuación. Verifica el resultado contra la función Decode de la biblioteca.

8. Analizador de opciones con valores largos. Extiende el programa Opciones para que acepte también la forma --archivo=ruta.txt además de -f ruta.txt. Usa Index para encontrar el signo igual y rebanadas para separar nombre y valor. Maneja el caso de --archivo= sin valor.

9. Código de salida encadenado. Escribe un programa que valide que todos sus argumentos son enteros dentro de un rango dado por variables de entorno MIN y MAX. Devuelve Success si todos son válidos y Failure si alguno no lo es. Luego escribe un guion de shell que lo use con && y || en tres escenarios distintos.

10. Partidas reproducibles. Modifica el juego Adivina para que, cuando reciba una semilla, escriba al final una línea con el formato SEMILLA=n SECRETO=m INTENTOS=k. Después escribe un guion que juegue cien partidas con semillas del 1 al 100 alimentando la entrada con echo y verifique que la estrategia de búsqueda binaria siempre gana dentro del límite de intentos.

11. Historial con estructura propia. Cambia el Vector de enteros del juego por un Vector de un tipo record que guarde el valor del intento y la pista devuelta. Al final, imprime una tabla con tres columnas: número de intento, valor y pista. Usa Head y Tail de Ada.Strings.Fixed para alinear las columnas.

12. Comparar con Elixir. Escribe el mismo juego de adivinar el número en Elixir usando IO.gets, String.trim, Integer.parse y :rand.uniform. Compara ambas versiones en tres dimensiones: cuántas líneas ocupa cada una, cómo maneja cada lenguaje la entrada inválida, y qué pasa en cada caso cuando se cierra la entrada estándar. Documenta qué garantías da el sistema de tipos de Ada que en Elixir hay que verificar en ejecución.

Qué sigue

En este capítulo el lenguaje dejó de ser una curiosidad histórica y se volvió una herramienta de trabajo. Vimos que un arreglo en Ada lleva sus propios bordes y que eso elimina la necesidad de pasar tamaños a mano; que los índices pueden ser enumeraciones y que eso convierte un error de recorrido en un error de compilación; que cuando el tamaño no se conoce hay contenedores estándar con la misma disciplina de tipos. Vimos que “cadena de texto” en Ada no es un tipo sino tres estrategias distintas, cada una con un compromiso explícito entre flexibilidad y previsibilidad de memoria. Vimos cómo un programa recibe argumentos que el kernel copió en su pila inicial durante el execve, cómo convertirlos con validación real, y cómo devolver un código de salida que el shell pueda interpretar. Y armamos un juego completo donde todas esas piezas trabajan juntas, incluyendo los casos que normalmente se ignoran: entrada inválida, fin de archivo, configuración incorrecta, límite de intentos.

Lo que falta es la razón por la que Ada aparece en un curso de sistemas operativos y no en un curso de programación general. Todo lo que escribimos hasta ahora es secuencial: una sola línea de ejecución, un solo hilo de control. Ada es uno de los pocos lenguajes de propósito general donde la concurrencia no es una biblioteca sino parte del lenguaje mismo, con sintaxis propia, verificaciones del compilador y un modelo de sincronización que evita las condiciones de carrera por construcción en lugar de por convención. Las task que definen unidades concurrentes, las entradas con entry y accept que sincronizan por cita, y los protected que dan exclusión mutua sin que el programador maneje candados a mano, son la implementación directa de los conceptos de sincronización que estudiamos en la parte teórica del curso.

En el capítulo 14 entramos en ese terreno: cómo se declara una tarea, qué hace exactamente el runtime cuando una tarea arranca, cómo funciona la cita entre dos tareas, qué garantiza un objeto protegido y cómo se relaciona todo eso con los hilos que el sistema operativo realmente planifica. Puedes revisar el recorrido completo del curso en el índice.