Ada práctico: arreglos, contenedores, cadenas y argumentos de línea de comandos
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.
| Atributo | Qué devuelve | Ejemplo sobre array (1 .. 5) |
|---|---|---|
A'First | primer índice válido | 1 |
A'Last | último índice válido | 5 |
A'Length | cantidad de elementos | 5 |
A'Range | el rango A'First .. A'Last | 1 .. 5 |
A'First (N) | primer índice de la dimensión N | para matrices |
A'Last (N) | último índice de la dimensión N | para matrices |
A'Length (N) | cantidad de elementos de la dimensión N | para 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 <> 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
| Estructura | Tamaño | Acceso por índice | Insertar al medio | Usa montón | Cuándo conviene |
|---|---|---|---|---|---|
array restringido | fijo en el tipo | O(1) | no aplica | no | tamaño conocido y constante |
array no restringido | fijo por objeto | O(1) | no aplica | no | funciones que aceptan cualquier largo |
Vectors | dinámico | O(1) | O(n) | sí | lista que crece por el final |
Doubly_Linked_Lists | dinámico | no hay | O(1) con cursor | sí | inserciones y borrados frecuentes |
Ordered_Maps | dinámico | por clave, O(log n) | no aplica | sí | diccionario recorrido en orden |
Hashed_Maps | dinámico | por clave, O(1) medio | no aplica | sí | diccionario 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ácter | Ancho | Repertorio | Tipo de cadena |
|---|---|---|---|
Character | 8 bits | ISO/IEC 8859-1, o Latin-1 | String |
Wide_Character | 16 bits | plano multilingüe básico de Unicode | Wide_String |
Wide_Wide_Character | 32 bits | Unicode completo, incluidos emoji | Wide_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
| Criterio | String | Bounded_String | Unbounded_String |
|---|---|---|---|
| Largo | fijo por objeto | variable hasta Max | libre |
| Memoria | pila o estática | pila o estática | montón |
| Costo de concatenar | copia a un objeto nuevo | copia dentro del búfer | posible asignación dinámica |
| Apto para tiempo real duro | sí | sí | normalmente no |
| Conversión al otro | ya es String | To_String / To_Bounded_String | To_String / To_Unbounded_String |
| Error típico | Constraint_Error por largo distinto | Length_Error al pasarse | agotamiento 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
| Elemento | Tipo | Qué hace |
|---|---|---|
Argument_Count | función que devuelve Natural | cantidad de argumentos, sin contar el nombre del programa |
Argument (N) | función que devuelve String | el argumento en la posición N, con N de 1 a Argument_Count |
Command_Name | función que devuelve String | el nombre con que se invocó el programa |
Set_Exit_Status (C) | procedimiento | fija el código que el proceso devolverá al terminar |
Success | constante | código de salida que indica éxito |
Failure | constante | có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ón | Qué lee | Deja el salto pendiente | Excepción típica |
|---|---|---|---|
Ada.Text_IO.Get_Line (función) | una línea completa como String | no | End_Error al final del flujo |
Ada.Text_IO.Get_Line (S, L) | hasta llenar S, con L caracteres leídos | no si cupo | End_Error |
Ada.Text_IO.Get (C) | un carácter | sí | End_Error |
Ada.Integer_Text_IO.Get (N) | un entero, salteando espacios | sí | Data_Error si no es número |
Ada.Float_Text_IO.Get (F) | un real | sí | Data_Error |
Ada.Text_IO.Skip_Line | descarta hasta el próximo salto | no | End_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
| Error | Causa | Síntoma | Solución |
|---|---|---|---|
Constraint_Error: index check failed | índice fuera del rango del arreglo | el programa termina con traza en ejecución | recorrer con for I in A'Range en lugar de rangos escritos a mano |
Constraint_Error: length check failed al asignar una cadena | el largo del literal no coincide con el del objeto String | falla en tiempo de ejecución o de compilación | usar constant String := "..." para deducir bordes, o Unbounded_String si el largo cambia |
| El programa no espera la respuesta del usuario | un Get numérico dejó el salto de línea sin consumir | Get_Line devuelve una cadena vacía de inmediato | leer siempre con Get_Line y convertir con 'Value, o llamar a Skip_Line tras cada Get |
| El programa se cae si el usuario escribe letras | Integer'Value levanta Constraint_Error sin manejador | traza de excepción no capturada | envolver la conversión en un bloque con exception when Constraint_Error |
| El programa se cuelga o repite al presionar Ctrl+D | no se maneja End_Error en el bucle de lectura | bucle infinito imprimiendo el mensaje de error | capturar Ada.Text_IO.End_Error y salir del bucle |
Argument (0) falla | el índice de Argument empieza en 1 | Constraint_Error en el primer acceso | usar Command_Name para el nombre del programa |
| Los argumentos llegan como una sola cadena con Alire | falta el formato correcto de la opción | el programa cree que recibió un solo argumento | usar alr run --args="a b c" con comillas |
| El sorteo devuelve siempre la misma secuencia | falta llamar a Reset sobre el generador | el juego es idéntico en cada ejecución | llamar a Sorteo.Reset (Generador) antes del primer Random |
| Los acentos salen como caracteres raros | GNAT no está interpretando el fuente como UTF-8 | mojibake en la salida | compilar con -gnatW8 y declararlo en el paquete Compiler del .gpr |
'Length de una cadena con acentos da más de lo esperado | String cuenta bytes, y UTF-8 usa varios por carácter | cálculos de ancho de columna equivocados | usar Wide_Wide_String con Ada.Strings.UTF_Encoding cuando importan los puntos de código |
El compilador rechaza Total / Cantidad | mezcla de Integer y Float sin conversión | error de tipos en compilación | convertir explícitamente: Float (Total) / Float (Cantidad) |
V.Length no se puede sumar a un Integer | Length devuelve Ada.Containers.Count_Type | error de tipos en compilación | convertir con Integer (V.Length) |
| Un cursor deja de funcionar tras insertar | la operación estructural invalidó el cursor | excepción al usarlo | volver a obtener el cursor tras la modificación, o iterar con for E of |
| El shell no distingue éxito de error | el programa nunca llamó a Set_Exit_Status | los encadenamientos con && y ` | |
Modificar la variable de un for da error de compilación | en Ada la variable de control es constante dentro del bucle | rechazo del compilador | usar while cuando el avance no es de uno en uno |
| Una rebanada tiene índices inesperados | las rebanadas conservan los índices del original | Cola'First no vale 1 | asignar 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.