Programación en Ada/Texto completo
Programación en Ada
Prólogo
El objetivo de este libro es aprender a programar en el lenguaje Ada, desde sus características más sencillas hasta las más avanzadas. Ada es un lenguaje potente, pero no por ello es más complicado que Pascal, por poner un ejemplo.
Los prerrequisitos son: nociones generales de programación y experiencia en otro lenguaje. Aunque si aún no se sabe programar se puede complementar con la lectura de otro manual destinado a ello.
Hay dos maneras de leer este libro. Una es desde el principio hasta el final, siguiendo el orden establecido en el índice y en los encabezados de cada sección. Otra es utilizar los enlaces libremente para saltar a los temas de interés elegidos por el lector.
Acerca de este libro
Este manual que se está editando ahora en Wikilibros, deriva del libro escrito por José Alfonso Malo Romero. Esa versión ya estaba licenciada según la GFDL, la misma licencia que usamos en Wikilibros.
El estado actual de desarrollo es: , lo que indica que aún hay trabajo por hacer. Recuerda que esto es un wiki: estás invitado a colaborar en la escritura de este libro. Es fácil, lee la bienvenida a los nuevos autores.
Si no quieres o no puedes contribuir pero quieres hacer sugerencias, como qué capítulos te gustaría ver más desarrollados o qué tema echas de menos, puedes hacerlo en la página de discusión.
Créditos y licencia
Los autores de «Programación en Ada» son:
- José Alfonso Malo Romero, por la versión 1.0.1 (2001 - 2002)
- Por la actual versión de es.wikibooks.org (2004-2005):
- Manuel Gómez (Contribuciones)
- Andrés Soliño (Contribuciones)
- Varios autores de «Ada Programming», por ciertos capítulos que han sido traducidos de ese libro (sus historiales así lo indican).
- Alvaro López Ortega, por la versión inicial de Programación en Ada/GLADE [1] (2001).
Si quieres colaborar, sigue los consejos de «Cómo colaborar». Cuando hayas contribuido a la escritura del libro, añade tu nombre a la lista de autores.
Introducción
La programación de computadores, esto es, la creación de software para ellos, es un proceso de escritura. Así como los escritores de libros y artículos de revistas, los programadores (escritores de programas) deben expresar sus ideas por medio de una lengua escrita. Sin embargo, a diferencia de los escritores de novelas, los programadores deben expresar sus textos en lenguajes de propósito especial, basados en las matemáticas, conocidos como lenguajes de programación.
Ada, es uno de entre muchos posibles lenguajes de programación. Fue diseñado con un claro propósito en mente: la calidad del producto. Entendiéndose por calidad, la confianza que los usuarios van a poder depositar en el programa.
Si bien es posible escribir cualquier programa en Ada, éste ha sido utilizado principalmente, en el desarrollo de software de control, de tiempo real y de misión crítica. Típicamente, estos sistemas son responsables de procesos industriales y militares, muy costosos, y en los cuales incluso vidas humanas dependen del buen funcionamiento del software. Es vital en tales sistemas, utilizar un lenguaje que como Ada, ayuda en la creación de software de alta calidad.
Por otro lado, Ada, como lenguaje que promueve las buenas prácticas en ingeniería del software, es muy usado en la enseñanza de la programación en muchas universidades de todo el mundo.
Veamos cuáles son las características más destacables del lenguaje.
Características principales
- Legibilidad
- Los programas profesionales se leen muchas más veces de las que se escriben, por tanto, conviene evitar una notación que permita escribir el programa fácilmente, pero que sea difícil leerlo excepto, quizás, por el autor original y no mucho tiempo después de escribirlo.
- Tipado fuerte
- Esto asegura que todo objeto tenga un conjunto de valores que esté claramente definido e impide la confusión entre conceptos lógicamente distintos. Como consecuencia, el compilador detecta más errores que en otros lenguajes.
- Construcción de grandes programas
- Se necesitan mecanismos de encapsulado para compilar separadamente y para gestionar bibliotecas de cara a crear programas transportables y mantenibles de cualquier tamaño.
- Manejo de excepciones
- Los programas reales raramente son totalmente correctos. Es necesario proporcionar medios para que el programa se pueda construir en capas y por partes, de tal forma que se puedan limitar las consecuencias de los errores que se presenten en cualquiera de las partes.
- Abstracción de datos
- Se puede obtener mayor transportabilidad y mejor mantenimiento si se pueden separar los detalles de la representación de los datos y las especificaciones de las operaciones lógicas sobre los mismos.
- Procesamiento paralelo
- Para muchas aplicaciones es importante que el programa se pueda implementar como una serie de actividades paralelas. Dotando al lenguaje de estos mecanismos, se evita tener que añadirlos por medio de llamadas al sistema operativo, con lo que se consigue mayor transportabilidad y fiabilidad.
- Unidades genéricas
- En muchos casos, la lógica de parte de un programa es independiente de los tipos de los valores que estén siendo manipulados. Para ello, se necesita un mecanismo que permita la creación de piezas de programa similares a partir de un único original. Esto es especialmente útil para la creación de bibliotecas.
Historia
La historia de Ada comienza en 1974 cuando el Ministerio de Defensa de los Estados Unidos llevó a cabo un estudio de los lenguajes de programación utilizados en sus proyectos y concluyó que COBOL era un estándar para procesado de datos y FORTRAN para cálculo científico. Sin embargo, la situación con respecto a los sistemas empotrados era diferente: el número de lenguajes en uso era enorme. Ante esta falta de estandarización que provocaba gastos inútiles, se propuso el uso de un único lenguaje para estos sistemas.
El primer paso del desarrollo fue la redacción en 1975 de un documento que perfilaba los requisitos que debía satisfacer el lenguaje. Después de varias modificaciones, en 1976 se produjo una versión de estos requisitos sobre la que se evaluaron varios lenguajes. El resultado fue que ninguno de los lenguajes existentes cumplía todos los requisitos. Así pues, el paso siguiente fue la aceptación de propuestas de diversos contratistas para el diseño de un nuevo lenguaje, de los que se eligieron cuatro de ellos. El siguiente paso fue el refinamiento de las propuestas elegidas y se revisaron las especificaciones para dar la versión definitiva conocida como Steelman.
La elección final del lenguaje se hizo en 1979 cuando se declaró vencedor el desarrollo de CII Honeywell Bull. Se decidió que se llamaría Ada en honor a Augusta Ada Byron, condesa de Lovelace (1815-1852), hija de Lord Byron, quien fue ayudante y patrocinadora de Charles Babbage trabajando en su máquina analítica mecánica, de hecho, está considerada por muchos como el primer programador de la historia.
En 1983 se publicó el primer estándar ISO de Ada, el conocido Manual de referencia de Ada o ARM.
La primera revisión del lenguaje vino en 1995, marcando las dos versiones históricas que existen hasta el momento: Ada 83 y Ada 95. La última revisión ha sido aprobada recientemente por ISO y popularmente recibe el nombre de Ada 2005.
Enlaces externos
Más sobre la historia de Ada en:
Manual de referencia
A menudo abreviado ARM o RM, el manual de referencia de Ada (nombre completo: Ada Reference Manual, ISO/IEC 8652:1995(E)), es el estándar oficial que define el lenguaje. A diferencia de otros documentos oficiales de ISO, el manual de Ada se puede reproducir libremente, lo cual es una gran ventaja para su difusión.
En este libro encontrarás enlaces al manual de referencia en cada sección acabada, en relación con el tema tratado. El ARM es el lugar donde todas las preguntas sobre Ada tienen respuesta, aunque puede ser un texto un tanto árido por su carácter de estándar oficial. En cualquier caso es la fuente más completa y exacta de información.
Otros documentos de cabecera de Ada son los Ada Rationale, documentos que acompañan al estándar y explican las justificaciones en el diseño de cada revisión del lenguaje.
Enlaces externos al Manual de Referencia y Rationale
- Ada 2005:
- Ada 95
- Ada 83:
Instalación de un compilador
En esta sección explicaremos cómo instalar un compilador de Ada. Existen dos opciones gratuitas:
- GNAT, compilador libre y gratuito.
- Versión demo de ObjectAda.
GNAT
Para más información, véase el artículo «GNAT» en Wikipedia. |
GNAT es el único compilador de Ada gratuito completamente funcional. De hecho es parte del GNU Compiler Collection y por tanto es software libre. Sin embargo, la empresa que lo mantiene AdaCore, ofrece contratos de mantenimiento de las últimas versiones, antes de hacerlas públicas.
GNAT es también el único compilador que implementa todos los anexos (capítulos opcionales) del estándar de Ada.
GNAT GPL Edition: Ada 2005
Esta es la versión pública más reciente de AdaCore. Ten en cuenta que esta versión sólo permite distribuir binarios bajo la licencia GPL (la misma que usa Linux). Como ventaja decir que esta versión tiene soporte de las nuevas características de Ada 2005. Esta es la versión recomendable para estudiantes y profesionales que no necesitan soporte y licencian bajo GPL.
Para profesionales que quieren soporte o vender software bajo su propia licencia, AdaCore vende la edición llamada GNAT Pro.
GNAT GPL Edition se puede descargar de libre.adacore.com.
GNAT 3.15p precompilado
Para los que quieren distribuir binarios bajo una licencia distinta a la GPL y no pueden permitirse comprar una licencia de GNAT Pro lo más recomendable es descargar el paquete precompilado de la versión 3.15p, la última publicada por AdaCore bajo licencia GMGPL. Existen versiones para Windows, GNU/Linux y Solaris.
Aunque la versión precompilada no se ha actualizado recientemente, aún es suficiente para la mayoría de usuarios. Esta versión ha pasado el Ada Conformity Assessment Test Suite.
El Libre Site también proporciona el IDE para GNAT: GPS.
Se recomienda descargar estos paquetes adicionalmente:
- Linux
- ftp://ftp.cs.kuleuven.ac.be/pub/Ada-Belgium/mirrors/gnu-ada/3.15p/gnat-3.15p-i686-pc-redhat71-gnu-bin.tar.gz
- ftp://ftp.cs.kuleuven.ac.be/pub/Ada-Belgium/mirrors/gnu-ada/3.15p/florist-3.15p-src.tgz
- ftp://ftp.cs.kuleuven.ac.be/pub/Ada-Belgium/mirrors/gnu-ada/3.15p/asis/asis-3.15p-src.tgz
- http://web.archive.org/web/20070820033755/http://libre.adacore.com/gps/gps-2.1.0-academic-x86-linux.tgz
- Windows
- ftp://ftp.cs.kuleuven.ac.be/pub/Ada-Belgium/mirrors/gnu-ada/3.15p/winnt/gnat-3.15p-nt.exe
- ftp://ftp.cs.kuleuven.ac.be/pub/Ada-Belgium/mirrors/gnu-ada/3.15p/winnt/gnatwin-3.15p.exe
- ftp://ftp.cs.kuleuven.ac.be/pub/Ada-Belgium/mirrors/gnu-ada/3.15p/asis/asis-3.15p-src.tgz
- http://libre.adacore.com/gps/gps-2.1.0-academic-x86-windows.exe
- OS/2
Paquetes precompilados formando parte de distribuciones
Existen varias distribuciones que contienen una copia del compilador GNAT, pero como consejo deberías comprobar la vesión de GCC:
gcc --version gcc (GCC) 3.4.3 Copyright (C) 2004 Free Software Foundation, Inc.
Versiones que comiencen con 3.3. tienen muchos problemas por lo que deberías actualizarla a una vesión 3.4..
Mac OS X
GNAT for Macintosh proporciona una versión de GNAT, integración con Xcode y varios bindings.
GNU
La mayoría de las versiones de GNU vienen con paquetes de GNAT como parte de la distribución de GCC. Estas versiones se pueden usar perfectamente en vez de la versión de Libre Site.
SuSE
Todas las versiones tienen un compilador GNAT incluído. La versión SuSE 9.2 y superiores contienen también paquetes de ASIS, Florist y GLADE.
Se necesitan estos paquetes:
gnat gnat-runtime
Debian GNU/Linux
En Debian, GNAT se puede instalar con esta orden:
aptitude install gnat
Otros paquetes interesantes para el desarrollo en Ada bajo Debian 3.1 Sarge son:
- gnat - The GNU Ada 95 compiler
- ada-mode - Ada mode for GNU Emacs and XEmacs
- ada-reference-manual - The standard describing the Ada 95 language
- adabrowse - HTML generator for Ada 95 library unit specifications
- adacgi - Ada CGI interface
- asis-programs - Ada Semantic Interface Specification (ASIS) example programs
- gch - Ada quality & style checker
- gnade-dev - Develoment files for the GNat Ada Database Environment
- gnat-3.3 - The GNU Ada compiler
- gnat-3.4 - The GNU Ada compiler
- gnat-doc - Documentation for the GNU Ada compiler
- gnat-gdb - Ada-aware version of GDB
- gnat-glade - Distributed Systems Annex for GNAT (GNU Ada 95 compiler)
- gnat-gps - The GNAT Programming System - advanced IDE for C and Ada
- libadabindx-dev - Ada binding to the X Window System and *tif
- libadasockets0-dev - bindings for socket services in Ada
- libasis-3.15p-1-dev - Ada Semantic Interface Specification (ASIS) headers and li braries
- libaunit-dev - AUnit, a unit testing framework for Ada
- libaws-dev - Ada Web Server development files
- libflorist-3.15p-1-dev - POSIX.5 Ada interface to operating system services
- libgnomeada-2.4 - Ada binding for the Gnome Library
- libgtkada-2.4 - Ada binding for the GTK library
- libopentoken-dev - OpenToken lexical analysis library for Ada
- libtexttools-dev - Ada and C++ library for writing console applications
- libxmlada1-dev - XML/Ada, a full XML suite for Ada programmers
- libasis-3.14p-1 - Ada Semantic Interface Specification (ASIS) runtime library
- libcharles0-dev - Data structure library for Ada95 modelled on the C++ STL
Gentoo GNU/Linux
Bajo Gentoo la instalación de GNAT es muy sencilla gracias al emerge:
emerge gnat
Mandrake
En Mandrake, GNAT se puede instalar con esta orden:
urpmi gnat
Windows
Tanto MinGW para Windows como Cygwin contienen un paquete de GNAT.
MinGW
MinGW - Minimalist GNU for Windows.
Esta lista puede ayudarte a instalarlo:
- Instalar MinGW-3.1.0-1.exe.
- extraer binutils-2.15.91-20040904-1.tar.gz.
- extraer mingw-runtime-3.5.tar.gz.
- extraer gcc-core-3.4.2-20040916-1.tar.gz.
- extraer gcc-ada-3.4.2-20040916-1.tar.gz.
- extraer gcc-g++-3.4.2-20040916-1.tar.gz (opcional).
- extraer gcc-g77-3.4.2-20040916-1.tar.gz (opcional).
- extraer gcc-java-3.4.2-20040916-1.tar.gz (opcional).
- extraer gcc-objc-3.4.2-20040916-1.tar.gz (opcional).
- extraer w32api-3.1.tar.gz.
- Instalar mingw32-make-3.80.0-3.exe (opcional).
- Instalar gdb-5.2.1-1.exe (opcional).
- Instalar MSYS-1.0.10.exe (opcional).
- Instalar msysDTK-1.0.1.exe (opcional).
- extraer msys-automake-1.8.2.tar.bz2 (opcional).
- extraer msys-autoconf-2.59.tar.bz2 (opcional).
- extraer msys-libtool-1.5.tar.bz2 (opcional).
Se recomienda usar D:\MinGW como directorio de instalación.
Es de notar que la versión para Windows del Libre Site también se basa en MinGW.
Cygwin
Cygwin, el entorno GNU para Windows, también contiene una versión de GNAT, aunque más antigua que la de MinGW, y no soporta DLLs ni multi-tarea (al 11/2004).
MS-DOS
DJGPP es un porte a MS-DOS de una selección de herramientas GNU con extensiones de 32 bits, que está mantenido activamente. Incluye la colección completa de compiladores de GCC, lo cual ahora incluye Ada. Véase el sitio de DJGPP para instrucciones de instalación.
Los programas de DJGPP, a parte de en modo nativo MS-DOS, se pueden ejecutar en ventanas de DOS en Windows.
Solaris 8, 9, 10 sobre SPARC y x86
Descarga GCC de blastwave.org. Es la suite completa con el compilador de GNAT.
Compilarlo uno mismo
Si quieres las últimas características, incluyendo el soporte experimental de Ada 2005, tendrás que compilar tú mismo el GNAT.
Instrucciones:
- Linux o Cygwin: véase ada.krischik.com.
- Windows: pendiente
Aonix ObjectAda
Aonix ObjectAda es un compilador comercial que proporciona una versión gratuita de evaluación, con limitaciones en el número y tamaño de archivos y en el número de controles que se pueden colocar en una ventana.
El IDE de ObjectAda es muy similar al de MS Visual C++ y al de Delphi e incluye un GUI builder.
Enlaces
Un ejemplo básico: Hola Mundo
Un ejemplo común de la sintaxis de un lenguaje es el programa Hola mundo. He aquí una implementación en Ada con la intención de ser un primer contacto.
with
Ada.Text_IO;procedure
Hola_Mundois
begin
Ada.Text_IO.Put_Line("¡Hola, mundo!"
);end
Hola_Mundo;
Por ahora puede ser suficiente con aprender a compilar y enlazar un programa escrito en Ada, pero si tienes curiosidad aquí va una explicación del programa.
La cláusula with establece una dependencia con el paquete Ada.Text_IO y hace disponible toda la funcionalidad relacionada con la Entrada/Salida de textos.
Después se define un procedimiento como programa principal. Nótese que en Ada no tiene que tener un nombre especial como main, simplemente ha de estar fuera de cualquier paquete. Al compilar se indica cuál es el programa principal.
Ada.Text_IO.Put_Line llama al procedimiento Put_Line definido en el paquete Ada.Text_IO para imprimir la cadena "¡Hola, mundo!"
.
Abreviando
Si no deseamos tener sentencias largas, como Ada.Text_IO.Put_Line, se pueden usar dos métodos para abreviar:
- Usar una cláusula use para hacer directamente visible todas las entidades definidas en Ada.Text_IO. No es recomendable abusar de esta cláusula por las razones explicadas aquí.
- Usar una cláusula renames, para renombrar Ada.Text_IO con un nombre más corto.
-- Con clásula use
with
Ada.Text_IO;use
Ada.Text_IO;procedure
Hola_Mundois
begin
Put_Line("¡Hola, mundo!"
);end
Hola_Mundo;
-- Con cláusula renames
with
Ada.Text_IO;use
Ada.Text_IO;procedure
Hola_Mundois
package
T_IOrenames
Ada.Text_IO;begin
T_IO.Put_Line("¡Hola, mundo!"
);end
Hola_Mundo;
Compilación
Como ejemplo, con el compilador GNAT este programa se debe escribir en un archivo llamado hola_mundo.adb (el nombre del procedimiento que contiene, más .adb) y se compilaría así:
gnatmake hola_mundo.adb
Naturalmente si usas un entorno integrado de desarrollo la compilación será una opción de menú o un botón de la barra de herramientas.
El resultado es un archivo ejecutable llamado hola_mundo que imprime ¡Hola, mundo! por su salida estándar (normalmente en una ventana en modo texto).
Elementos del lenguaje
Alfabeto
El alfabeto de Ada consta de:
- Letras mayúsculas: A, ..., Z y minúsculas: a, ..., z.
- Dígitos: 0, ..., 9.
- Caracteres especiales.
Es de destacar que en Ada 95 se admiten caracteres como 'Ñ', 'Ç' y vocales acentuadas ya que se permiten los 256 caracteres comprendidos en ISO Latin-1.
El alfabeto de minúsculas puede usarse en vez de o junto con el alfabeto de mayúsculas, pero se considera que los dos son idénticos (a excepción de las cadenas de caracteres y literales tipo carácter).
Componentes léxicos
Se pueden encontrar en Ada los siguientes componentes léxicos:
- Identificadores
- Literales numéricos
- Literales de tipo carácter
- Cadenas de caracteres
- Delimitadores
- Comentarios
- Palabras reservadas
Hacer constar, que el espacio no constituye nada más que un separador de elementos léxicos, pero es muy importante utilizarlos para una mayor legibilidad, tanto dentro de las sentencias, como elemento de sangrado para ayudar a diferenciar los bloques.
Ejemplo:
Temperatura_Sala := 25; -- Temperatura que debe tener la sala.
Esta línea contiene 5 elementos léxicos:
- El identificador
Temperatura_Sala
- El delimitador compuesto
:=
- El número
25
- El delimitador simple
;
- El comentario
-- Temperatura que debe tener la sala.
Identificadores
Definición en BNF:
identificador ::= letra { [ subrayado ] letra | cifra } letra ::= A | ... | Z | a | ... | z cifra ::= 0 | ... | 9 subrayado ::= _
Aunque dentro de esta definición entrarían las palabras reservadas que tienen un significado propio en el lenguaje y, por tanto, no pueden ser utilizadas como identificadores.
Nota: en la versión Ada 95 se incorporan los caracteres de Latin-1, con lo que se pueden escribir identificadores como Año o Diámetro.
No hay límite en el número de caracteres de un identificador, aunque todo identificador deberá caber en una única línea.
_Hora_Del_Día: no, porque comienza por guión bajo.
Inicio_: no, porque termina por guión bajo.
Mañana: sí.
Hora_Del_Día: sí.
Jabalí: sí.
contador: sí.
2a_vuelta: no, porque comienza por número.
ALARMA: sí.
Access: no, es una palabra reservada de Ada.
Precio_en_$: no, contiene un carácter ($) que no es letra, cifra ni guión bajo.
alarma__general: no, porque contiene dos guiones bajos seguidos.
HoraDelDía: sí.
Números
Los literales numéricos constan de:
- dígitos
0 .. 9
- el separador de decimales
.
- el símbolo de exponenciación
e
oE
- el símbolo de negativo
-
- el separador
_
Como ejemplo, el número real 98,4 se puede representar como: 9.84E1
,
98.4e0
, 984.0e-1
ó 0.984E+2
. No estaría permitido 984e-1
.
Para representación de número enteros, por ejemplo 1.900, se puede
utilizar 19E2
, 190e+1
ó 1_900E+0
. Sirviendo el carácter _
como mero
separador para una mejor visualización.
Una última característica es la posibilidad de expresar un literal
numérico en una base distinta de 10 encerrando el número entre
caracteres #
, precedido por la base: un número entre 2 y 16. Por ejemplo, 2#101#
equivale a 101 en base binaria, es decir, al número 5 en decimal. Otro
ejemplo con exponente sería 16#B#E2
que es igual a 11 × 16² = 2.816 (nótese que es 16² y no 10² porque la base en este caso es 16).
Literales de tipo carácter
Contienen un único carácter, por ejemplo: A
. Aquí sí se diferencian
mayúsculas de minúsculas. Se delimitan por un apóstrofe.
Ejemplos:
'A' 'ñ' '%'
Cadenas de caracteres
Contienen uno o varios caracteres y se delimitan por el carácter de
dobles comillas: "
, por ejemplo: "ABC"
. En este caso
se diferencian mayúsculas de minúsculas.
Delimitadores
Los delimitadores pueden ser uno de los siguientes caracteres especiales:
& ' ( ) * + , - . / : ; < = >
O ser uno de los delimitadores compuestos por dos caracteres especiales:
=> .. ** := /= >= <= << >> <>
Comentarios
Los comentarios se utilizan para ayudar a comprender los programas y lo constituye toda parte de texto precedida de dos guiones (--) hasta el fin de línea. No existe la posibilidad de insertar otro elemento léxico en la misma línea a partir de los dos guiones, es decir, el resto de la línea se interpreta como comentario en su totalidad.
-- Este comentario ocupa una línea completa.
Mis_Ahorros := Mis_Ahorros * 10.0;-- Este está después de una sentencia.
Mis_Ahorros := Mis_Ahorros *-- Este está entre medias de una sentencia que ocupa dos líneas.
100_000_000.0;
Palabras reservadas
Como el resto de los elementos léxicos, las palabras reservadas de Ada son equivalentes tanto en mayúsculas como en minúsculas. El estilo más extendido es escribirlas completamente en minúsculas.
En Ada las palabras reservadas pueden tener un uso distinto dependiendo del contexto, los distintos usos de cada una se puede consultar en el capítulo Palabras reservadas.
Manual de referencia de Ada
Tipos
Clasificación de tipos
Los tipos de Ada se pueden clasificar en:
- Escalar:
- Discreto:
- Enteros: Integer, Natural, Positive.
- Enumeraciones: Boolean, Character.
- Real:
- Discreto:
Declaración de tipos
Para definir un tipo no estándar, se puede emplear el siguiente esquema:
declaración_tipo ::=type
identificadoris
definición_tipo ;
Sirvan como ejemplos las siguientes definiciones de tipos:
-- Escalares discretos no estándar:
--
type
TIndiceis
range
1..50;-- Escalares reales no estándar:
--
type
TPesois
digits
10;-- Coma flotante con precisión de 10 cifras.
type
TMasais
delta
0.01range
-12.0 .. 12.0;-- Coma fija 0.01 precis.
-- Enumeración:
--
type
TColoris
(ROJO, VERDE, AZUL);-- Vectores:
--
type
TMatrizis
array
(1..10, 1..10)of
Float;type
TVector5is
array
(TIndicerange
5..10)of
Float;-- Registros:
--
type
TVálvulais
record
Nombre: String(1..20); Abierta: Boolean; VelocidadLíquido: Floatrange
0.0 .. 30.0;end
record
;-- Punteros:
--
type
PEnterois
access
Integer;
-- Arrays irrestringidos. Tienen un número indefinido de
-- elementos. Es necesario especificar los límites al declarar
-- variables de ese tipo.
--
declare
type
TVectorIndefis
array
(Integerrange
<>)of
Float; V: TVectorIndef (1..4);begin
V(1) := 10.28; V := (1.2, 1.5, 1.8, 1.3); V := (1 => 1.2, 2 => 1.7,others
=> 0.0);end
;
Algunos atributos aplicables a tipos
Los atributos son operaciones predefinidas que se pueden aplicar a tipos, objetos y otras entidades. Por ejemplo estos son algunos atributos aplicables a tipos:
- Last: Integer'Last es el máximo valor que puede tomar la variable de tipo Integer. También es el último valor de un tipo enumerado o del índice de un vector.
- First: Integer'First es el mínimo valor que puede tomar la variable de tipo Integer. También es el primer valor de un tipo enumerado o del índice de un vector.
- Range: Vector'Range indica el rango que ocupa la variable Vector, es decir, equivale a Vector'First..Vector'Last. En el caso de más de una dimensión, el valor Matriz'Range(1) indica el rango de la primera dimensión.
- Succ: TColor'Succ(ROJO) indica el siguiente valor a ROJO que toma el tipo TColor, si no existe, se eleva la excepción Constraint_Error.
- Pred: TDía'Pred(VIERNES) indica el anterior valor a VIERNES que toma el tipo TDía, si no existe, se eleva la excepción Constraint_Error.
- Pos: El atributo Pos indica la posición ocupada por un determinado valor en un tipo enumeración. Por ejemplo: TColor'Pos(ROJO). La primera posición se considera 0.
- Val: El atributo Val indica el valor que ocupa una determinada posición en un tipo enumeración. Por ejemplo: COLOR'Val(1).
- Para identificar unívocamente un valor de un tipo enumeración se emplea TColor' (ROJO) y TIndicador'(ROJO) para distinguir el valor ROJO del tipo TColor o TIndicador.
Subtipos
Los subtipos definen un subconjunto de los valores de un tipo determinado, pero no son un tipo distinto de su tipo base.
Superar una ambigüedad
En el supuesto caso de que se quiera superar una ambigüedad en el tipo de una variable (debería evitarse) en un determinado instante, se puede optar por convertirlo (no recomendable) o cualificarlo:
- Convertir el tipo, por ejemplo: Integer(I) convierte el número I a entero.
- Cualificar el tipo, por ejemplo: Integer'(I) interpreta I como entero.
En ambos casos, el resultado es I como entero.
La cualificación sirve para cuando un literal no se sabe a qué tipo pertenece y se le indica de ese modo. Por ejemplo:
...
type Color is (Rojo, Azul, Naranja);
type Fruta is (Naranja, Pera, Melon);
procedure Put (Una_Fruta : Fruta);
procedure Put (Un_Color : Color);
...
begin
Put (Color'(Naranja)); -- Llama a procedure Put (Un_Color : Color)
Put (Fruta'(Naranja)); -- Llama a procedure Put(Una_Fruta : Fruta)
Put (Naranja); -- Error de sintaxis, la llamada es ambigua
Put (Rojo); -- Ok, llama a procedure Put (Un_Color : Color)
end;
Tipos avanzados
Hay otros tipos más avanzados en Ada. Puedes optar por leer estos tipos ahora o por continuar el libro por la siguiente lección Objetos (variables y constantes).
- Registros discriminados
- Registros variantes
- Punteros a objetos
- Punteros a subprogramas
- Tipos derivados
- Tipos etiquetados
Manual de referencia de Ada
Enteros
Tipos enteros con signo
Un tipo entero con signo se define declarando un rango, por ejemplo:
type
Índiceis
range
1 .. 50;
Los extremos del rango se pueden consultar con los atributos 'First y el 'Last del tipo.
Cuando se asigna un valor a una variable de este tipo, se chequea en tiempo de ejecución si pertenece al rango. En caso negativo se levanta la excepción Constraint_Error.
El compilador puede definir el tamaño más eficiente para el tipo a no ser que se defina una cláusula de representación.
type
Índiceis
range
1 .. 50;for
Índice'Sizeuse
8;
Ejemplo
El siguiente ejemplo, tomado del wikilibro en inglés sobre Ada, define un nuevo rango de -5 a 10 e imprime el rango completo.
with
Ada.Text_IO;procedure
Range_1is
type
Range_Typeis
range
-5 .. 10;package
T_IOrenames
Ada.Text_IO;package
I_IOis
new
Ada.Text_IO.Integer_IO (Range_Type);begin
for
Ain
Range_Typeloop
I_IO.Put ( Item => A, Width => 3, Base => 10);if
A < Range_Type'Lastthen
T_IO.Put (",");else
T_IO.New_Line;end
if
;end
loop
;end
Range_1;
Tipos enteros con signo predefinidos
En el paquete Standard se predefinen varios tipos enteros con signo, como Integer (y sus subtipos Natural y Positive), Short_Integer, Long_Integer y posiblemente otros (dependiendo del compilador). Estos tipos tienen los tamaños más adecuados para la arquitectura del computador. Si no tienes razones para definir nuevos tipos enteros, considera usar estos tipos o un subtipo de ellos.
Esta sería una posible representación gráfica de los subtipos de Integer:
Enteros modulares
Los enteros modulares no tienen signo y son cíclicos, es decir no se produce desbordamiento (overflow) sino wrap-around. Para estos tipos están predefinidos los operadores lógicos (and, or, xor) para realizar operaciones a nivel de bits.
Se definen así:
type
Nombreis
mod
Módulo;
Donde el 'First es 0 y 'Last es Módulo - 1. Como los tipos modulares son cíclicos y .
Se puede definir un subtipo de un tipo modular:
type
Byteis
mod
256;subtype
Medio_Byteis
Byterange
0 .. 127;
Pero, cuidado porque el módulo de Medio_Byte sigue siendo 256.
Manual de referencia de Ada
Enumeraciones
Un tipo enumeración es una lista definida de los posibles valores que puede adoptar un objeto de ese tipo. La declaración:
type
Color_Primariois
(Rojo, Verde, Azul);
establece que a un objeto del tipo Color_Primario se le puede asignar cualquiera de los tres valores indicados, y ningún otro valor. Como los tipos numéricos, donde por ejemplo 1 es un literal entero, Rojo, Verde y Azul son los llamados literales del tipo.
Las enumeraciones son uno de los tipos discretos, los otros son los tipos enteros.
Operadores y atributos predefinidos
Aparte del operador de igualdad ("="), los tipos enumeración tienen predefinidos todos los operadores de orden: "<", "<=", "=", "/=", ">=", ">"; donde la relación de orden viene dada implicitamente por la secuencia de los literales: cada literal tiene una posición, empezando por 0 para el primero, que se incrementa en uno para cada sucesor. Esta posición se puede obtener mediante el atributo 'Pos; el inverso es 'Val, que devuelve el valor correspondiente a una posición. En nuestro ejemplo:
Color_Primario'Pos (Rojo) = 0 Color_Primario'Val (0) = Rojo
Los literales pueden sobrecargarse, es decir, puede haber otro tipo con los mismos literales.
type
Luz_de_Tráficois
(Rojo, Ambar, Verde);
Normalmente se puede determinar por el contexto a qué tipo pertenece el literal Rojo. Cuando es imposible, el programador ha de usar una expresión de desambiguación: Tipo'(Valor).
Literales carácter
Una característica bastante única de Ada es la posibilidad de usar literales caracter como literales de una enumeración.
type
ABCis
('A', 'B', 'C');
El literal 'A' no tiene mucho que ver con el literal 'A' del tipo predefinido Character (o Wide_Character).
Todo tipo que tiene al menos un literal carácter es un tipo carácter. Para todo tipo carácter, existen literales de cadena y el operador de concatenación predefinido "&".
type
Mi_Caracteris
(No_Caracter, 'a', Literal, 'z');type
Mi_Stringis
array
(Positiverange
<>)of
Mi_Caracter; S: Mi_String :="aa"
& Literal &"za"
& 'z'; T: Mi_String := ('a', 'a', Literal, 'z', 'a', 'z');
En este ejemplo, S y T contienen el mismo valor.
El tipo predefinido Character se define de este modo en el paquete Standard.
Tipo boolean
El tipo predefinido boolean es una enumeración definida en el paquete Standard de este modo:
type
Booleanis
(False, True);
Por esta razón el tipo boolean tiene todos los atributos definidos para cualquier otra enumeración.
Subtipos de enumeración
Se puede usar un rango para definir un subtipo sobre una enumeración:
subtype
Letra_Mayúsculais
Characterrange
'A' .. 'Z';subtype
Color_Fríois
Color_Primariorange
Verde .. Azul;
Manual de referencia de Ada
Coma flotante
Para definir un tipo de coma flotante es suficiente con definir cuantos dígitos se necesitan de este modo:
digits
Digitos
Si se desea también se puede definir el rango mínimo:
digits
Digitosrange
Primero .. Último
Esta capacidad es uno de los grandes beneficios de Ada sobre la mayoría de los lenguajes de programación en este respecto. Otros lenguajes, sólo proporcionan un tipo float y otro long float, y lo que la mayoría de los programadores hacen es:
- elegir float si no están interesados en la precisión
- de lo contrario, eligen long float, puesto que es lo mejor que pueden obtener.
En cualquiera de los dos casos, el programador no sabe cual es la precisión que obtiene.
En Ada, uno especifica la precisión necesitada y el compilador elige el tipo de coma flotante que cumple al menos esa precisión. De este modo el requisito se cumple. Además, si la máquina tiene más de dos tipos de coma flotante, el compilador puede hacer uso de todos ellos.
Por supuesto, el programador también puede hacer uso de los tipos de coma flotante predefinidos que son Float y posiblemente (si el compilador lo implementa) Short_Float, Short_Short_Float, Long_Float y Long_Long_Float.
Manual de referencia de Ada
Para más información, véase el artículo «Coma flotante» en Wikipedia. |
Coma fija
Coma fija decimal
Es posible definir un tipo de coma fija decimal declarando el delta (el error absoluto) y la cantidad de dígitos en base decimal necesitados (incluyendo la parte real):
delta
Deltadigits
Digitos
Delta ha de ser una potencia de 10, si no, el tipo no será de coma fija decimal.
También podemos definir el rango mínimo necesitado:
delta
Deltadigits
Digitosrange
Primero .. Último
Ejemplo:
type
T_Precio_en_Eurosis
delta
0.01digits
15;
Coma fija ordinaria
Para un tipo de coma fija ordinaria (binaria) se especifica simplemente el delta con un rango.
delta
Deltarange
Primero .. Último
Ejemplo:
type
T_Medidais
delta
0.125range
0.0 .. 255.0;
Manual de referencia Ada
Para más información, véase el artículo «Coma fija» en Wikipedia. |
Arrays
Un array es una colección de elementos a los que se puede acceder por su índice. En Ada todo tipo definitivo (aquel del que se conoce su tamaño) se permite como elemento y cualquier tipo discreto, (es decir, enteros con signo y modulares, o enumeraciones) puede usarse como índice.
Declaración de arrays
Los arrays de Ada son de alto nivel, comparados, por ejemplo, con los de C/C++. Esto se traduce en varias posibilidades sintácticas que se presentan a continuación.
Sintáxis básica
La declaración básica de un array es la siguiente:
array
(Tipo_Índice)of
Tipo_Elemento
Este array consiste en un elemento de tipo Tipo_Elemento por cada posible valor de Tipo_Índice. Por ejemplo, si quisieramos contar las ocurrencias de cada letra en un texto nos definiríamos un array de este tipo:
type
Contador_Caracteresis
array
(Character)of
Natural;
- Nota: usamos Natural como tipo de elemento puesto que los valores negativos de Integer no tienen sentido en una cuenta. Es conveniente usar el subtipo entero más adecuado en cada caso, puesto que así nos beneficiamos de la comprobación de rango y podemos descubrir errores fácilmente.
Con subrango conocido
A menudo no necesitamos un array con todos los valores posibles del tipo del índice. En este caso definimos un subtipo del tipo índice con el rango necesitado.
subtype
Subtipo_Índiceis
Tipo_Índicerange
Primero .. Último;array
(Subtipo_Índice)of
Tipo_Elemento;
Para lo que hay una forma más abreviada si no deseamos definir el subtipo con nombre, se puede hacer anónimamente:
array
(Tipo_Índicerange
Primero .. Último)of
Tipo_Elemento;
Puesto que Primero y Último son expresiones del tipo Tipo_Índice, una forma más simple es la siguiente:
array
(Primero .. Último)of
Tipo_Elemento
Ten en cuenta que si First y Last son literales numéricos, esto implica que el tipo índice base es el Integer.
Si en el ejemplo anterior, sólo deseásemos contar letras mayúsculas desechando otros caracteres, podríamos definir el tipo array de este modo:
type
Contador_Caracteresis
array
(Characterrange
'A' .. 'Z')of
Natural;
Con un subrango desconocido
A menudo el rango necesitado no se conoce hasta tiempo de ejecución o necesitamos objetos array de varias longitudes. En lenguajes de más bajo nivel como C necesitaríamos hacer uso de la memoria dinámica (del heap). Pero no es el caso de Ada, puesto que la caja <> nos permite declarar arrays de tamaño no restringido:
array
(Tipo_Índicerange
<>)of
Tipo_Elemento
Cuando declaramos objetos de este tipo, los extremos (bounds) del array deben conocerse, bien como resultado de una función o por una inicialización mediante un agregado. Desde su declaración hasta su finalización, el objeto no puede cambiar de tamaño.
Con elementos aliased
Los programadores de C/C++ dan por hecho que todo elemento de un array tiene una dirección propia en memoria (de hecho el nombre del array es un puntero sobre el que se puede operar).
En Ada, esto no es siempre así. Veamos este ejemplo:
type
Día_De_Mesis
range
1 .. 31;type
Día_Con_Citais
array
(Día_De_Mes)of
Boolean;pragma
Pack (Día_Con_Cita);
Puesto que hemos empaquetado el array, el compilador usará el mínimo espacio de almacenamiento posible. En la mayoría de los casos esto implica que los 8 valores booleanos cabrán en un byte.
Pero este no es el único caso en el que el compilador de Ada puede empaquetar un array puesto que tiene libertad en los casos en que sea más optimo.
Si queremos acceder con un puntero a cada elemento tenemos que expresarlo explícitamente.
type
Día_De_Mesis
range
1 .. 31;type
Día_Con_Citais
array
(Día_De_Mes)of
aliased
Boolean;
Uso de arrays
Para acceder a los elementos de un array se usan el nombre del objeto array seguido del índice entre paréntesis.
Se puede acceder a una rodaja (slice) de un array usando (x .. y).
Vector_A (1 .. 3) := Vector_B (4 .. 6);
El operador "&" permite concatenar arrays:
Nombre_Completo := Nombre & ' ' & Apellidos;
Si se intenta acceder a un elemento más alla de los límites del array o se asigna a un array (completo o slice) un array de distinto tamaño se levanta la excepción Constraint_Error (a menos que los chequeos estén deshabilitados).
Ejemplo de uso
with
Ada.Text_IO, Ada.Integer_Text_IO;use
Ada.Text_IO, Ada.Integer_Text_IO;procedure
Agendais
type
Dia_De_Mesis
range 1 .. 31;type
Dia_Con_Citais
array (Dia_De_Mes)of
Boolean; Citas_En_Mayo : Dia_Con_Cita := (others
=> False);-- Se inicializa todo el mes a False
begin
-- Tengo citas los dias 3, del 8 al 16 (excepto el 14), y el último día del mes.
Citas_En_Mayo (3) := True; Citas_En_Mayo (8 .. 16) := (others
=> True); Citas_En_Mayo (14) := False; Citas_En_Mayo (Citas_En_Mayo'Last) := True; Put ("En mayo tienes citas los dias:"
);for
Iin
Citas_En_Mayo'Rangeloop
if
Citas_En_Mayo (I)then
Put (Dia_De_Mes'Image (I));end
if
;end
loop
;end
Agenda;
Manual de referencia de Ada
Strings
En Ada los strings son un tipo especial de array que está implicitamente definido en el paquete Standard como:
type
Stringis
array
(Positiverange
<>)of
Character;
Que el rango se defina sobre el subtipo Positive implica que ningún string de Ada puede empezar en 0. Esta es una diferencia con los strings de C/C++, la otra es que un string de Ada no tiene por qué terminar en NUL (carácter de código 0), de hecho puede tener caracteres NUL intercalados. Esto es así porque los arrays de Ada siempre llevan asociados los límites (atributos 'First y 'Last).
Los literales de tipo String se encierran entre comillas.
Al ser String un array no restringido no podemos definir variables de ese tipo sin definir explícitamente o implícitamente los límites del array.
Nombre : String (1 .. 8); -- Explícitamente
Nombre : String :="Fulanito"
;-- Implícitamente
El operador & está definido para concatenar strings entre sí y caracteres con strings.
Nombre_Completo : constant
String := Nombre & ' ' & Apellidos;
Puesto que hay dos tipos de caracteres, hay también dos tipos de strings: String y Wide_String que es un array de Wide_Character. En Ada 2005 aparece también el tipo Wide_Wide_String cuyos elementos son Wide_Wide_Character.
Para facilitar el uso de los strings hay varios paquetes predefinidos para su manejo:
- Ada.Strings.Fixed: para strings de tamaño fijo.
- Ada.Strings.Bounded: para strings con longitud variable y un limite superior definido.
- Ada.Strings.Unbounded: para strings con longitud variable y sin límites de tamaño.
Para manejo de Wide_Strings existen otros tres paquetes predefinidos que se nombran anteponiendo Wide_ a cada uno.
Manual de refencia de Ada
Registros
Un registro es un tipo que almacena varios campos de tipos distintos y que se identifican por su nombre. En C/C++ se llaman struct y en Pascal y Ada record.
type
T_Sensoris
record
Magnitud : Natural; Alarma : Boolean;end
record
;
Acceso a los campos
Para acceder a los campos de un registro se usa la clásica notación registro.componente:
procedure
Medir_temperaturais
Sensor_temperatura : T_Sensor;begin
Sensor_temperatura.Magnitud := 23; Sensor_temperatura.Alarma := False;-- ...
if
Sensor_temperatura.Alarmathen
Put_Line ("Alarma de temperatura"
);end
if
;end
;
Para asignar valores a todos los componentes se puede utilizar un agregado, lo cual es útil para asegurarnos de que no dejamos ninguno sin darle un valor:
procedure
Asignar_temperaturais
Sensor_temperatura : T_Sensor;begin
Sensor_temperatura := (Magnitud => 23, Alarma => False);end
;
También es posible dar un valor inicial a algunos o todos los componentes de un registro de modo que todos los objetos de ese tipo se inicialicen automáticamente con esos valores:
type
T_Sensoris
record
Magnitud : Natural := 0; Alarma : Boolean := False;end
record
;
Registro nulo
El registro nulo se puede usar cuando se necesita un tipo que no almacene ningún dato (aunque parezca raro hay casos en los que es útil, como instanciar un genérico que pide un tipo, para el cual no tenemos ningún dato que pasarle). Hay dos maneras de declarar un registro nulo:
type
Registro_Nulois
record
null
;end
record
;
type
Registro_Nulois
null
record
;
Ambas definiciones son semánticamente idénticas, la segunda es simplemente una sintaxis abreviada.
La declaración de una variable o constante de este tipo sería:
Nulo :constant
Registro_Nulo := (null
record
);
Tipos especiales de registros
- Registros con discriminante
- Registros con parte variante
- Registros etiquetados, dan soporte de la programación orientada a objetos en Ada 95.
Manual de referencia de Ada
Registros discriminados
En un tipo registro discriminado, a algunos de los componentes se los conoce como discriminantes y el resto pueden depender de ellos. Los discriminantes tienen que ser de tipo discreto o puntero. Por ejemplo:
type
T_Matrizis
array
(Integerrange
<>, Integerrange
<>)of
Float;type
T_Matriz_Cuadrada (Orden: Positive)is
record
Matriz: T_Matriz(1..Orden, 1..Orden);end
record
;
De esta forma, se asegura que la matriz utilizada sea cuadrada. El primer componente (Orden) es el discriminante del subtipo discreto Positive, mientras que el segundo componente (Matriz) es una matriz siempre cuadrada y cuyos límites dependen del valor de Orden. Ahora se utilizaría de la siguiente manera:
M: T_Matriz_Cuadrada(3);-- También se puede emplear M: T_Matriz_Cuadrada(Orden => 3);
M := (3, (1..3 => (1..3 => 0.0))); M := (M.Orden, (M.Matriz'Range(1) => (M.Matriz'Range(2) => 5.0)));-- En la sentencia anterior M.orden ya está definido e igual a 3.
Una vez que se declara la variable ya no se puede cambiar su restricción. Sin embargo, si se declara lo siguiente:
Max:constant
:= 100;subtype
T_Índiceis
Integerrange
0..Max;type
T_Vector_Enterosis
array
(Integerrange
<>)of
Integer;type
T_Polinomio (N: T_Índice := 0)is
-- Discriminante con valor inicial.
record
Pol: T_Vector_Enteros(0..N);end
record
;
Ahora, se pueden declarar variables que no tengan restricciones:
P, Q: T_Polinomio;
El valor inicial de sus discriminantes sería cero (el valor por defecto de N). Así, se podría cambiar el discriminante posteriormente de esta manera:
P: T_Polinomio := (3, (5, 0, 4, 2));
R: T_Polinomio(5); -- Aquí sólo se podrían usar polinomios de grado 5.
Manual de referencia Ada
Registros variantes
Puede interesar que un registro contenga partes de su estructura que puedan variar en función de otras. Para ello se definen los registros variantes que son un tipo especial de registros discriminados en los que la existencia de algunos campos depende del valor del discriminante.
Por ejemplo:
declare
type
TTipoVuelois
(MILITAR, CIVIL, ENEMIGO, DESCONOCIDO);type
TRegistroVuelo (Clasif: TTipoVuelo)is
record
Velocidad: Float; Detalles: String(1..100);case
Clasifis
when
CIVIL =>null
;when
MILITAR => Origen, Destino: String(1..10);when
ENEMIGO | DESCONOCIDO => NivelAmenaza: Integer;end
case
;end
record
; Vuelo1: TRegistroVuelo (MILITAR);begin
Vuelo1.Origen :="Francia "
;end
;
Sería ilegal la asignación Vuelo1.NivelAmenaza := 1;
pues dicho campo sólo es válido
para DESCONOCIDO
o ENEMIGO
.
Si el discriminante lleva un valor por defecto entonces el registro es variante durante su ciclo de vida (es mutable). Si no, el valor del discriminante se fija en su declaración y no varía durante la vida del objeto.
Los registros variantes en cierto modo están sobrepasados por la extensión de los tipos etiquetados, sin embargo aún son útiles en casos simples como aquellos en los que se conocen todas las posibilidades y no se desea que alguien añada una derivación a posteriori.
Véase también
Manual de referencia de Ada
Punteros a objetos
Un nombre está ligado a un objeto desde su declaración hasta que el flujo del programa deja la unidad que contenía su declaración. Sin embargo, los punteros o apuntadores (access) proporcionan acceso a otros objetos, que se pueden crear y destruir dinámicamente.
El nombre de access en vez del habitual pointer se debe a que al diseñar Ada se quería huir de la mala fama que los punteros habían creado gracias a lenguajes como C, en los que se puede usar los punteros de manera muy insegura. Los tipos acceso de Ada son más seguros entre otras cosas porque no existe la aritmética de punteros, especialmente peligrosa. Además el uso de punteros en Ada es prescindible en muchas más situaciones que en C.
Las variables de tipo puntero en Ada se inicializan implícitamente a null.
Ejemplos
Por ejemplo, se puede definir un puntero a un tipo entero de esta manera:
type
PEnterois
access
Integer;
En un ejemplo con registros:
declare
type
TBúferis
record
Mensaje: String(1..4); Prioridad: Integer;end
record
;type
PTBúferis
access
TBúfer; Mensaje1, Mensaje2: PTBúfer;begin
Mensaje1 :=new
TBúfer;-- Se crea un objeto de tipo TBúfer.
Mensaje2 :=new
TBúfer'(Prioridad => 2, Mensaje =>"Hola"
);-- Con all se puede desreferenciar el puntero.
-- Mensaje1 es un puntero y Mensaje1.all es el registro.
Mensaje1.all
.Prioridad := 3;-- Sin embargo, al acceder a campos del registro la desreferenciación
-- puede hacerse implícita y .all es opcional en esos casos:
Mensaje1.Prioridad := 3;end
;
Es útil para implementar listas, colas, árboles y grafos. Por ejemplo:
declare
-- TNodoÁrbolBinario se necesita para definir el puntero.
type
TNodoÁrbolBinario;-- Se declara después.
type
PTNodoÁrbolBinariois
access
TNodoÁrbolBinario;type
TNodoÁrbolBinariois
record
RamaIzda: PTNodoÁrbolBinario; Dato: Float; RamaDcha: PTNodoÁrbolBinario;end
record
; ÁrbolBinario: PTNodoÁrbolBinario;begin
-- Se crea la raíz del árbol binario.
ÁrbolBinario :=new
TNodoÁrbolBinario'(null
, 1.0,null
);end
;
Liberación de memoria
Cuando se quiera liberar la memoria dinámicamente, hay que hacer uso del procedimiento genérico Ada.Unchecked_Deallocation [2], el cual se instancia con los tipos de objeto y de puntero, y se le llama pasándole punteros. Por ejemplo:
with
Ada.Unchecked_Deallocation;procedure
Ejemplo_Liberar_Memoriais
type
TVectoris
array
(Integerrange
<>)of
Float;type
PVectoris
access
TVector; PV: PVector;procedure
Liberar_Vectoris
new
Ada.Unchecked_Deallocation (TVector, PVector);begin
PV :=new
TVector(1..10); PV.all
:= (others => 0.0);-- ...
Liberar_Vector (PV);-- La memoria es liberada y PV es ahora null
end
Ejemplo_Liberar_Memoria;
El nombre de Unchecked_Deallocation viene del hecho de que no hay comprobación de que no queden punteros colgantes (dangling pointers), es decir que si se ha copiado el puntero en otra variable, después de llamar a Liberar_Vector el puntero copia está apuntando a una dirección de memoria no reservada y los efectos son imprevisibles, puesto que se puede haber reservado y se pude escribir o leer memoria que ya no pertenece a ese objeto.
Este sistema es similar al de C++ con new y delete. Un sistema de recolección de basura similar al de Java está previsto en el estándar, pero ningún compilador de Ada hasta el momento lo proporciona. Esto es debido a que aunque es un mecanismo más seguro, es menos eficiente y puede ser un problema para los sistemas de tiempo real por su impredictibilidad.
En Ada 95 existen métodos de gestión de memoria más seguros que el uso directo de Unchecked_Deallocation basados en los tipos controlados, algo semejante a lo que se consigue en C++ con constructores y destructores que manejan memoria dinámica.
Manual de referencia de Ada
Punteros a subprogramas
Un puntero a subprograma nos permite llamar a un subprograma sin conocer su nombre ni dónde está declarado. Este tipo de punteros se suele utilizar en los conocidos callbacks.
type
TPCallbackis
access
procedure
(Id : Integer; Mensaje : String);
type
TFCallbackis
access
function
(Mensaje : String)return
Natural;
Para obtener el valor del puntero se usa el atributo 'Access aplicado a un subprograma con el prototipo adecuado, es decir, han de coincidir orden y tipo de los parámetros, y en el caso de las funciones, el tipo de retorno.
procedure
ProcesarEvento (Id : Integer; Mensaje : String);
MiCallback : TPCallback := ProcesarEvento'Access;
Los punteros a subprograma fueron una de las extensiones de Ada 95.
Manual de referencia de Ada
Tipos derivados
Hay ocasiones en las que es útil introducir un tipo nuevo que es similar en la mayoría de los aspectos a uno ya existente, pero que es un tipo distinto. Con la siguiente sentencia, se dice que S es un tipo derivado de T:
type
Sis
new
T;
Características
Los aspectos que definen a un tipo son su conjunto de valores y su conjunto de operaciones. El conjunto de valores de un tipo derivado es una copia del conjunto de valores del progenitor y, al ser copia, no pueden asignarse entre ambos. El conjunto de operaciones aplicables a un tipo derivado son:
- Los atributos son los mismos.
- A no ser que el tipo progenitor sea limitado, posee la asignación, igualdad y desigualdad predefinidas.
- El tipo derivado heredará los subprogramas primitivos del tipo progenitor, es decir, los subprogramas que tengan algún parámetro o un resultado de dicho tipo, y bien sean predefinidos o estén definidos en el mismo paquete que el tipo progenitor.
Estas operaciones heredadas, a efectos de uso, es como si estuvieran definidas en la misma región declarativa que el tipo derivado.
Ejemplo
Por ejemplo, si se tiene
, entonces el
tipo derivado TEnteroNuevo heredará los subprogramas predefinidos como
"+", "-", "abs", etc. y los atributos como First o Last.
type
TEnteroNuevo is
new
Integer
Así, por ejemplo, se puede escribir:
type
TNumManzanasis
new
Integer; NumManzanas: TNumManzanas;-- ...
NumManzanas := NumManzanas + 1;
Como puede observarse, se hace uso del operador "+" predefinido para el tipo Integer.
Tipos derivados frente a subtipos
¿Cuándo crear un tipo derivado en vez de un subtipo? Cuando queremos tener un nuevo tipo cuyos valores no deseamos que se confundan con los del tipo original pero queremos que posea las mismas operaciones y la misma representación que el tipo base. Por ejemplo:
-- Supongamos que tenemos cajas en las que caben 20 piezas de frutas
subtype
TNumFrutasis
Naturalrange
1 .. 20;type
TNumManzanasis
new
TNumFrutas;type
TNumPerasis
new
TNumFrutas; NumManzanas: TNumManzanas; NumPeras: TNumPeras; NumFrutas: TNumFrutas;-- ...
-- Error de compilación, no podemos juntar peras con manzanas:
NumManzanas := NumManzanas + NumPeras;-- Ilegal
-- Pero siempre podemos contabilizarlos como frutas convirtiendo al tipo
-- base:
NumFrutas := TNumFrutas (NumManzanas) + TNumFrutas (NumPeras);
Nota para programadores de C/C++: typedef
realmente no define un tipo, sino que define un nuevo nombre para un mismo tipo, algo parecido al subtype, pero nada que ver con el
.
type
S is
new
T
Manual de referencia de Ada
Tipos etiquetados
Ada 83 fue diseñado para dar soporte a desarrollos de sistemas empotrados, de tiempo real y misión crítica. Para este tipo de trabajos, las características de Ada, como el tipado fuerte y la división en paquetes, permitían el uso de una serie de metodologías de desarrollo, con un razonable grado de comodidad. Sin embargo, en los años 90, el advenimiento de la programación orientada a objectos, hizo necesario que Ada se extendiera para dar cabida a esta nueva metodología. Esto debido, a que la programación orientada a objeto necesitaba de un soporte en el manejo de tipos y en la visibilidad de los métodos, que Ada 83 hubiera considerado como no permitido.
Los cambios necesarios, fueron incorporados al siguiente estándar: Ada 95.
Básicamente, los cambios se centraron alrededor del uso de los tipos etiquetados, una construcción software que añade flexibilidad al tipado fuerte, justo en grado necesario para permitir representar en Ada un diagrama de objectos, por ejemplo, en UML.
Los tipos etiquetados de Ada 95 son los que permiten realizar el polimorfismo. A menudo se asimila la combinación de un tipo etiquetado y el paquete que lo contiene a una clase en otros lenguajes de programación también orientados a objetos. Sin embargo hay algunas diferencias de sintaxis, que no de filosofía general, que veremos en este apartado.
Un tipo etiquetado puede ser un registro de modo que su estructura es pública, o puede ser un tipo privado, aunque en la parte privada siempre se termina definiendo como un registro.
type
T_Baseis
tagged
record
...end
record
;
type
T_Baseis
tagged
private
;
Un tipo etiquetado se puede extender.
type
T_Derivadois
new
T_Basewith
record
...end
record
;
type
T_Derivadois
new
T_Basewith
private
;
El nuevo tipo hereda todos las operaciones primitivas del tipo base y las puede redefinir.
Todo objeto de un tipo etiquetado contiene una etiqueta (tag) que permite reconocer su tipo en tiempo de ejecución.
Tipos polimórficos (class-wide type)
En Ada el polimorfismo se consigue con un tipo especial que puede contener objetos de cualquier tipo derivado de uno dado. Estos tipos especiales se indican con el atributo Class, es decir, T_Base'Class puede almacenar cualquier objeto derivado del tipo T_Base.
Este tipo de la clase es un tipo irrestringido, es decir, para declarar un objeto de este tipo tenemos que inicializarlo llamando a una función, asignándole otro objeto o con un new (si es un puntero a tipo polimórfico, es decir, access all T_Base'Class).
Llamadas que despachan
La ventaja del polimorfismo es poder escribir algoritmos que no dependen del tipo concreto de los objetos que se manejan.
El polimorfismo dinámico que proporciona la programación orientada a objetos permite que una operación sobre un objeto polimórfico se realice de distinto modo dependiendo del tipo concreto del objeto de que se trate en la ejecución. Otras formas de polimorfismo son el polimorfismo paramétrico que se implementa en Ada con las unidades genéricas y el polimorfismo por sobrecarga.
En Ada este polimorfismo se consigue llamando a una operación primitiva de un objeto polimórfico. Cuando el objeto sobre el que se realiza la operación es polimórfico y el parámetro formal del subprograma llamado es concreto, el subprograma realmente llamado puede ser uno redefinido para el tipo concreto si se ha redefinido para él una implementación distinta de la del tipo base.
Nota para programadores de C++: esto quiere decir que en Ada todos los "métodos" son virtuales, pero no todas las llamadas son virtuales, sólo las que llaman a un método de un objeto polimórfico (semejante a un puntero a clase base).
Manual de referencia de Ada
Subtipos
Los subtipos se emplean para definir un subconjunto de un tipo determinado definido por una restricción.
Esta restricción puede ser un rango para un tipo escalar:
subtype
TDíaDelMesis
Integerrange
1..31;subtype
TDíaFebrerois
TDíaDelMesrange
1..29;subtype
TLaborableis
TDíaDeSemanarange
Lunes..Viernes;
O una restricción en un array irrestringido:
type
TMatrizis
array
(Positive range <>, Positiverange
<>)of
Integer;subtype
TMatriz10x10is
TMatriz (1 .. 10, 1 .. 10);
O una restricción en el valor de un registro discriminado:
type
TPersona (Sexo : TSexo)is
record
Nombre : TNombre;case
Sexois
when
Mujer => Embarazada : Boolean;when
Hombre =>null
;end
case
;end
record
;subtype
TMujeris
TPersona (Sexo => Mujer);
Los subtipos de un mismo tipo base son totalmente compatibles entre sí, es decir, no es necesaria una conversión de tipos para asignar objetos de subtipos distintos. Sin embargo, si en tiempo de ejecución se asigna un objeto a una variable y no se cumplen las restricciones del subtipo, se levantará la excepción Constraint_Error.
Manual de referencia de Ada
Objetos
Los objetos son entidades que se crean en tiempo de ejecución y contienen un valor de un determinado tipo. En Ada los objetos se clasifican en variables y constantes.
Nótese que el concepto de objeto no implica necesariamente el uso del paradigma de la orientación a objetos. Para programar orientado a objetos en Ada se utilizan los objetos de tipo etiquetado.
Variables
Una variable se introduce en el programa mediante una declaración, que se podría denotar así:
declaración_variable ::= identificador { , identificador } : tipo [ := expresión ] ;
Por ejemplo:
V: Boolean := TRUE;
I, J: Integer := 1;
Nombre: String := "Wikilibros"
;
Destino_A_Calcular: Coordenadas;
Constantes
Una constante es un objeto que se inicializa a un valor cuando se declara y posteriormente no puede cambiar.
Una constante se declara igual que una variable, pero añadiendo la
palabra reservada constant
:
declaración_constante ::= identificador { , identificador } : constant
[ tipo] [ := expresión ] ;
Por ejemplo:
PI: constant
Float := 3.14159_26536;
Un tipo especial de constante es el número nombrado para el cual no es necesario especificar un tipo y que es equivalente a usar el literal correspondiente.
OtroPI:constant
:= 3.14;-- En este caso es de tipo universal_float.
Manual de referencia de Ada
Atributos
Los atributos son operaciones predefinidas que se pueden aplicar a tipos, objetos y otras entidades. Tienen la siguiente sintaxis:
Entidad'Atributo
o bien
Tipo'Atributo(Entidad)
Atributos aplicables a tipos
Por ejemplo estos son algunos atributos aplicables a tipos (por orden alfabético):
- Ceiling. Se usa con la forma X'Ceiling(Y), siendo X cualquier tipo flotante e Y una variable de ese tipo. Devuelve el "techo" de Y, es decir el valor entero más pequeño que es mayor o igual que Y.
- Digits. Representa el número de decimales de la mantisa de un tipo flotante.
- First. Indica el mínimo valor que puede tomar una variable de un tipo discreto, sea entero o enumeración. Se usa con la forma T'First, siendo T un tipo discreto (entero o enumeración).
- Image. Se usa con la forma X'Image(Y), siendo X cualquier tipo discreto e Y una variable de ese tipo. Devuelve una cadena (String) con la representación del valor de la variable Y.
- Last. Indica el máximo valor que puede tomar una variable de un tipo discreto, sea entero o enumeración.
- Pos. Indica la posición ocupada por un determinado valor en un tipo enumeración. Por ejemplo: TColor'Pos(ROJO). La primera posición se considera 0.
- Pred. TDía'Pred(VIERNES) indica el anterior valor a VIERNES que toma el tipo TDía, si no existe, se eleva la excepción Constraint_Error.
- Rounding. Se usa con la forma X'Rounding(Y), siendo X cualquier tipo flotante e Y una variable de ese tipo. Devuelve el valor de Y redondeado al entero más cercano. Si Y está exactamente entre dos valores enteros, toma el valor del mayor entero (p.e, Float'rounding(1.5)=2.0).
- Size. Indica el mínimo espacio en bits en que se pueden almacenar objetos de este tipo. Técnicamente se define como lo que ocuparía un componente de un registro de este tipo cuando el registro está empaquetado (pragma Pack).
- Succ. TColor'Succ(ROJO) indica el siguiente valor a ROJO que toma el tipo TColor, si no existe, se eleva la excepción Constraint_Error.
- Truncation. Se usa con la forma X'Truncation(Y), siendo X cualquier tipo flotante e Y una variable de ese tipo. Devuelve el valor truncado de Y a un valor entero.
- Val. Indica el valor que ocupa una determinada posición en un tipo enumeración. Por ejemplo: COLOR'Val(1).
Atributos aplicables a objetos
Por ejemplo estos son algunos atributos aplicables a objetos:
- Size: tamaño en bits de un objeto.
- Valid: si tiene una representación válida para su tipo. Útil cuando se obtienen valores desde el «mundo exterior» mediante Unchecked_Conversion u otro mecanismo.
- First, Last: aplicados a arrays dan el primer y el último índices del array.
- Range: Vector'Range indica el rango que ocupa la variable Vector, es decir, equivale a Vector'First..Vector'Last. En el caso de más de una dimensión, el valor Matriz'Range(1) indica el rango de la primera dimensión.
Ejemplos
type
Tipo_enumeradois
(Enum1, Enum2, Enum3);for
Tipo_enumerado'Sizeuse
2;-- Para representar 3 unidades necesitamos 2 bits
type
Tipo_enterois
range
-1 .. 5; ...pragma
Assert (Tipo_enumerado'Last = Enum3);-- Correcto
pragma
Assert (Tipo_entero'First = -1);-- Correcto
pragma
Assert (Tipo_entero'Last = 4);-- ¡¡Incorrecto!!
pragma
Assert (Tipo_enumerado'Pred( Enum2 ) = Enum1);-- Correcto
pragma
Assert (Tipo_enumerado'Succ( Enum2 ) = Enum3);-- Correcto
pragma
Assert (Tipo_enumerado'Image( Enum1 ) = "Enum1");-- Correcto
type
Tipo_flotanteis
digits
10range
0.0..100.0;-- 10 cifras decimales en la mantisa
Var_Flotante : Float := 1.5; Var_Flotante2 : Float := 1.9; Var_Flotante3 : Float := 1.0; Var_Flotante4 : Float := -1.8; Var_Flotante5 : Float := 1.1; ...pragma
Assert (Float'Ceiling(var_Flotante) = 2.0);-- Correcto
pragma
Assert (Float'Ceiling(var_Flotante2) = 2.0);-- Correcto
pragma
Assert (Float'Ceiling(var_Flotante3) = 1.0);-- Correcto
pragma
Assert (Float'Ceiling(var_Flotante3) = 2.0);-- ¡¡Incorrecto!!
pragma
Assert (Float'Truncation(var_Flotante) = 1.0);-- Correcto
pragma
Assert (Float'Truncation(var_Flotante2) = 1.0);-- Correcto
pragma
Assert (Float'Truncation(var_Flotante3) = 1.0);-- Correcto
pragma
Assert (Float'Truncation(var_Flotante4) = -1.0);-- Correcto
pragma
Assert (Float'Rounding(var_Flotante5) = 1.0);-- Correcto
pragma
Assert (Float'Rounding(var_Flotante) = 2.0);-- Correcto
A : Character := Character'Val (32)-- A toma el valor de espacio (valor 32 en la tabla ASCII)
B : Character := ' ';-- B también toma el valor de espacio
Resultado : Natural; ...if
not
Resultado'Validthen
-- 'Resultado' está fuera del rango, con valor negativo
Result := Natural'First;end
if
Manual de referencia de Ada
Expresiones
Una expresión es una parte de un programa que calcula u obtiene un valor. Toda expresión tiene un tipo asociado que es el del valor calculado/obtenido. Una expresión está formada por una combinación de valores, objetos, operadores, atributos y funciones.
Notación
Una expresión en Ada se puede construir de la siguiente manera simplificada:
expresión ::= relación operador relación relación ::= expresión_simple [ operador_relacional expresión_simple ] | expresión_simple [ not ] in rango expresión_simple ::= [ operador_unario ] término { operador_binario término }
Manual de referencia de Ada
Operadores
Clasificación
Ésta es la lista de operadores de Ada de menor a mayor precedencia.
Clase | Nombre | Fórmula | Sintáxis Ada |
---|---|---|---|
Lógicos | Conjunción | and
| |
Disyunción inclusiva | or
| ||
Disyunción exclusiva | xor
| ||
Relacionales | Igualdad | = | |
Desigualdad | /= | ||
Menor que | < | ||
Menor o igual que | <= | ||
Mayor que | > | ||
Mayor o igual que | >= | ||
Aditivo unario | Identidad | + | |
Negación | - | ||
Aditivo binario | Suma | + | |
Resta | - | ||
Concatenación | Concatenación | & | |
Multiplicativo | Multiplicación | * | |
División | / | ||
Módulo | mod
| ||
Resto | rem
| ||
De máxima precedencia | Exponenciación | ** | |
Negación | not
| ||
Valor absoluto | abs
|
Propiedades
En todos los casos, excepto para la exponenciación, los dos operandos deben ser del mismo tipo.
Los operadores se pueden sobrecargar.
Comprobación de pertenencia (in, not in)
Además existe la comprobación de pertenencia ( , in
; , not
in
) que técnicamente no es
un operador y no se puede sobrecargar. Su precedencia es la misma que la de los operadores relacionales. Se puede utilizar con rangos o con subtipos.
-- Supongamos que X es de tipo Integer
if
Xin
Positivethen
-- Positive es un subtipo de Integer
...if
Xnot
in
4 .. 6then
...end
if
;end
if
;
declare
type
Dia_Semanais
(Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo);subtype
Dia_Laborableis
Dia_Semanarange
Lunes .. Viernes; Hoy : Dia_Semana := Obtener_Dia;begin
if
Hoyin
Dia_Laborablethen
-- Dia_Laborable es un subtipo de Dia_Semana
Ir_Al_Trabajo;if
Hoynot
in
Lunes .. Miercolesthen
Pensar_En_El_Fin_De_Semana;end
if
;end
if
;end
;
Operadores lógicos de cortocircuito
Para los operadores lógicos existen versiones para minimizar las evaluaciones (short-circuit evaluation). Es decir, se evalúa primero el operando de la izquierda y después, sólo si es necesario para determinar el resultado, el de la derecha:
- Conjunción
and
then
: no se evalúa la segunda expresión si la primera es falsa porque ya sabemos que el resultado será falso. - Disyunción inclusiva
or
else
: no se evalúa la segunda expresión si la primera es verdadera porque ya sabemos que el resultado será verdadero.
-- B / A > 3 no se ejecutará si A es 0 lo que nos será útil para evitar
-- un Constraint_Error
--
if
A /= 0and
then
B / A > 3then
Put_Line ("B / A es mayor que 3"
);end
if
;
Manual de referencia de Ada
Sentencias y estructuras de control
Clasificación de sentencias
- Simple:
- Secuencial:
- Nula (
null
). - Asignación (X := Y;)
- Llamada a procedimiento.
- Código.
- Llamada a punto de entrada (
entry
). En procesamiento paralelo. - Espera (
delay
). - Terminación abrupta (
abort
). En procesamiento paralelo.
- Nula (
- Control:
- Terminación de bucle (
exit
). - Salto a etiquetas (
goto
). No recomendable. - Elevación de excepción (
raise
). - Retorno de subprograma (
return
).
- Terminación de bucle (
- Secuencial:
- Compuesta:
- Secuencial:
- Paralela:
Bloques
Ada distingue entre declaraciones, que introducen identificadores nuevos, y sentencias, que no lo hacen. El fragmento de texto más simple que incluye declaraciones y sentencias es un bloque.
Definición
bloque ::= [ identificador : ] [declare
parte_declarativa ]begin
sentencias [exception
manejador_de_excepción { manejador_de_excepción } ]end
[ identificador ] ;
Ejemplo
-- Supuestas X e Y declaradas con anterioridad de tipo Float:
declare
Temp: Float;-- Esta variable solo es visible dentro del bloque.
begin
-- Se intercambian 2 variables.
Temp := X; X := Y; Y := Temp;end
;
Destacar que un bloque es una sentencia y cuando se ejecuta se elaboran las declaraciones contenidas en su parte declarativa (entre declare y begin) y después se ejecutan las sentencias del cuerpo (entre begin y end).
El ámbito de las variables declaradas en un bloque finaliza cuando termina dicho bloque.
Sentencia de selección (if)
Definición
sentencia_selección ::=if
condiciónthen
secuencia_de_sentencias [ {elsif
condiciónthen
secuencia_de_sentencias } ] [else
secuencia_de_sentencias ]end
if
;
with
Ada.Text_Io,Ada.Integer_Text_Io;use
Ada.Text_Io,Ada.Integer_Text_Io;
procedure
comptar_LAis
cp,c: character; n: integer; --declaramos las variablesbegin
Put("Escribe una frase"); --pedimos y recogemos texto Put(c); n := 0; --inicializamos n a cero cp := ' '; get(c); --si cp es espacio seguir leyendowhile
c /= '.'loop
--mientras c sea diferente del PUNTO, entrar en bucleif
cp = 'L'and
c = 'A'then
n := n+1;end
if
; -- si cp es L y c es A incrementar n y finalizar el if cp := c; Get(c); --igualamos cp a c para proceder y c sigue al próximo caracterend
loop
; --FINALIZAMOS BUCLE Put("El numero de palabras de la frase es"); --enseñamos en pantalla la variable n Put(n);end
comptar_LA;
Ejemplo
if
Hoy = DOMthen
-- Si hoy es domingo.
Mañana := LUN;elsif
Hoy /= SABthen
-- Si no es domingo ni sábado.
Laborable := True;else
-- Cualquier otro caso.
Mañana := TDía'Succ(Hoy);end
if
;
Sentencia de selección por casos (case)
Definición
sentencia_selección_por_casos ::=case
expresiónis
alternativa_caso { alternativa_caso }end
case
; alternativa_caso ::=when
elección { | elección } => secuencia_de_sentencias elección ::= expresión_simple | rango_discreto |others
Ejemplo
case
Hoyis
when
MIE..VIE => Entrenar_duro;-- Rango.
when
MAR | SAB => Entrenar_poco;-- Varias elecciones.
when
DOM => Competir;-- Una elección.
when
others
=> Descansar;-- Debe ser única y la última alternativa.
end
case
;
Bucles
Sentencia de bucle simple (loop)
Definición
sentencia_bucle_simple ::= [ identificador_bucle : ]loop
secuencia_de_sentenciasend
loop
[ identificador_bucle ] ;
Ejemplo
Vida:loop
-- El bucle dura indefinidamente.
Trabajar; Comer; Dormir;end
loop
Vida;
Este bucle sólo se puede abandonar si alguno de los procedimientos levanta una excepción.
El bucle simple a menudo se acompaña de una sentencia exit para abandonar el bucle cuando se cumple una condición.
loop
Alimentar_Caldera; Monitorizar_Sensor;exit
when
Temperatura_Ideal;end
loop
;
Sentencia de bucle iterativo (for)
Definición
sentencia_bucle_iterativo ::= [ identificador_bucle : ]for
parámetros_forloop
secuencia_de_sentenciasend
loop
[ identificador_bucle ] ; parámetros_for ::= identificadorin
[reverse
] rango_discreto
Ejemplo
for
Iin
1..Nloop
-- I se itera desde 1 hasta N.
V(I) := 0;end
loop
;
Ejemplo 2
Se desea desplegar los números del 10 al 1 de forma descendente
procedure
bloque_para_inversois
i :Integer
;begin
i := 0;for
iin reverse
1..10loop
Put
(i, 0);Put
(" ");end
loop
;end
bloque_para_inverso;
Ejemplo 3
Si bien Ada no acepta rangos en el for, estos se puede hacer mediante cálculos. Se desea mostrar los números pares comprendidos entre 1 y 10.
procedure
bloque_para_inversois
i :Integer
;begin
i := 0;for
i in 0..5loop
Put
(i*2, 0);Put
(" ");end
loop
;end
bloque_para_inverso;
Sentencia de bucle condicional (while)
Definición
sentencia_bucle_iterativo ::= [ identificador_bucle : ]while
condiciónloop
secuencia_de_sentenciasend
loop
[ identificador_bucle ] ;
Ejemplo
I := 1;while
I > Nloop
-- Se hace el bucle mientras se cumpla la condición.
V(I) := 0; I := I + 1;end
loop
;
Otras sentencias de control (goto, exit, return, abort)
Sentencia goto
Antes de nada, decir que la utilización de sentencias goto se desaconseja totalmente. Ada posee estructuras de control adecuadas para evitar su uso. Si se soporta su utilización es por si se quiere traducir de otro lenguaje a Ada automáticamente.
Se especifica una etiqueta entre los símbolos << y >>, por ejemplo, <<Salto>>.
Se realiza el salto a dicha etiqueta con la sentencia
que
transferiría el control a la siguiente sentencia después de la etiqueta.
goto
Salto;
No puede usarse para transferir control fuera de un subprograma.
Sentencia exit
Termina el bucle nombrado en la sentencia o, si no aparece, el bucle más próximo que la contiene. Su notación sintáctica es:
sentencia_exit ::=exit
[ nombre_bucle ] [when
condición ] ;
Sentencia return
Termina la ejecución del subprograma más próximo que la contiene, tanto en procedimientos como en funciones donde, además, se utiliza para devolver el resultado de dicha función. Su notación sintáctica es:
sentencia_return ::= return
[ expresión ] ;
Sentencia abort
Se utiliza solo para tareas. Implica la terminación incondicional de las tareas que se especifiquen. Su notación sintáctica es:
sentencia_abort ::= abort
nombre_tarea { , nombre_tarea } ;
Manual de referencia de Ada
Subprogramas
En Ada, los subprogramas se dividen en dos categorías: procedimientos y funciones. Los procedimientos son llamados como sentencias y no devuelven resultado, mientras que las funciones son llamadas como componentes de expresiones y devuelven un resultado.
Procedimientos
La llamada a un procedimiento en Ada constituye por sí misma una sentencia. Su especificación se puede describir de la siguiente manera:
especificación_procedimiento ::=procedure
identificador [ ( especificación_parámetro { ; especificación_parámetro } ) ] especificación_parámetro ::= identificador { , identificador } : [ modo ] tipo [ := expresión ] modo ::=in
|out
|in
out
El cuerpo del procedimiento sería:
cuerpo_procedimiento ::= especificación_procedimientois
[ parte_declarativa ]begin
secuencia_de_sentencias [exception
manejador_de_excepción { manejador_de_excepción } ]end
[ identificador ] ;
Los parámetros que se le pueden pasar a un procedimiento pueden ser de tres modos diferentes:
in
: el parámetro formal es una constante y permite sólo la lectura del valor del parámetro real asociado.
in
out
: el parámetro formal es una variable y permite la lectura y modificación del valor del parámetro real asociado.
out
: el parámetro formal es una variable y permite únicamente la modificación del valor del parámetro real asociado.
Ninguno de estos modos implica el uso de un paso de parámetro por valor o por referencia. El compilador es libre de elegir el paso más adecuado de acuerdo al tamaño del parámetro y otras consideraciones. Como excepción, los objetos de tipos etiquetados se pasan siempre por referencia.
Por ejemplo:
procedure
Una_Prueba (A, B:in
Integer; C:out
Integer)is
begin
C:= A + B;end
Una_Prueba;
Cuando se llame al procedimiento con la sentencia Una_Prueba (5 + P, 48,
Q); se evalúan las expresiones 5 + P y 48 (sólo se permiten expresiones
en el modo in), después se asignan a los parámetros formales A y B, que
se comportan como constantes. A continuación, se asigna el valor A + B a
la variable formal C. Obsérvese que especificando el modo out no se
puede conocer el valor del parámetro real (Q). En este caso, el
parámetro formal C es una nueva variable cuyo valor se asignará al
parámetro real (Q) cuando finalice el procedimiento. Si se hubiera
querido obtener el valor de Q, además de poder modificarlo, se debería
haber empleado C: in out Integer
.
Indicar también que dentro de un procedimiento, se puede hacer uso de la sentencia return sin argumentos que finalizaría la ejecución del procedimiento y pasaría el control a la sentencia desde la que se llamó a dicho procedimiento. Por ejemplo, para resolver una ecuación del tipo :
with
Ada.Numerics.Elementary_Functions;use
Ada.Numerics.Elementary_Functions;procedure
Ecuación_Cuadrática (A, B, C : Float;-- Por defecto es in.
R1, R2 :out
Float; Válida :out
Boolean)is
Z: Float;begin
Z := B * B - 4.0 * A * C;if
Z < 0.0or
A = 0.0then
Válida := False;-- Al ser de salida, se tienen que modificar al menos una vez.
R1 := 0.0; R2 := 0.0;else
Válida := True; R1 := (-B + SQRT (Z)) / (2.0*A); R2 := (-B - SQRT (Z)) / (2.0*A);end
if
;end
Ecuación_Cuadrática;
Siendo SQRT la función de Ada.Numerics.Elementary_Functions que calcula la raíz cuadrada del parámetro pasado. Si las raíces son reales, se devuelven en R1 y R2, pero si son complejas o la ecuación degenera (A = 0), finaliza la ejecución del procedimiento después de asignar a la variable Válida el valor False, para que se controle después de la llamada al procedimiento. Nótese que los parámetros out tienen que modificarse, al menos, una vez y que si no se especifica un modo, se sobreentiende que es in.
Funciones
Una función es una forma de subprograma a la que se puede invocar como parte de una expresión. Su especificación se puede describir de la siguiente manera:
especificación_función ::=function
( identificador | símbolo_operador ) [ ( especificación_parámetro { ; especificación parámetro } ) ]return
tipo especificación_parámetro ::= identificador { , identificador } : [in
] tipo [ := expresión ]
Nótese que, al contrario que los procedimientos, no se pueden pasar parámetros a la función de otro modo que no sea de entrada (in) ya que no se puede especificar otro modo (el de entrada es por defecto y obligatorio). En este sentido las funciones de Ada se parecen más a las funciones matemáticas que las funciones de otros lenguajes.
La especificación de la función es necesaria para mostrar al exterior toda la información necesaria para poder invocarla.
El cuerpo de la función sería:
cuerpo_función ::= especificación_funciónis
[ parte_declarativa ]begin
secuencia_de_sentencias [exception
manejador_de_excepción { manejador_de_excepción } ]end
[ identificador | símbolo_operador ] ;
Un ejemplo de cuerpo de función puede ser:
function
Mínimo (A, B: Integer)return
Integeris
begin
if
A > Bthen
return
B;else
return
A;end
if
;end
Mínimo;
Los parámetros formales de una función se comportan como constantes locales cuyos valores son proporcionados por los parámetros reales correspondientes.
La sentencia return se utiliza para indicar el valor devuelto por la llamada a la función y para devolver el control a la expresión que llamó a la función. La expresión de la sentencia return es de complejidad arbitraria y debe ser del mismo tipo que se declara en la especificación de la función. Si se utiliza un tipo incompatible, el compilador da error. Si no se cumplen las restricciónes de un subtipo, como un rango, se eleva la excepción Constraint_Error.
El cuerpo de la función puede contener varias sentencias return y la ejecución de cualquiera de ellas terminará la función devolviendo el control a la sentencia que la había invocado. Si el flujo del programa sigue varios caminos dentro de la función hay que asegurarse de que se termine siempre con una sentencia return en cada uno de ellos. Si en tiempo de ejecución se llega al final de una función sin haber salido por un return, se levanta la excepción Program_Error. Así pues, el cuerpo de una función deberá tener al menos una sentencia return obligatoriamente.
Toda llamada a una función produce una nueva copia de cualquier objeto declarado dentro de ella, incluyendo los parámetros. Cuando la función finaliza, desaparecen sus objetos. Por tanto, es posible utilizar llamadas recursivas a una misma función, como ejemplo, se muestra una posible implementación de la función factorial:
function
Factorial (N: Positive)return
Positiveis
begin
if
N = 1then
return
1;else
return
(N * Factorial (N - 1));end
if
;end
Factorial;
Si se intenta evaluar la expresión Factorial (4); se llamará a la función con el parámetro 4 y dentro de la función se intentará evaluar la expresión N * Factorial (3) con lo que se volvería a llamar a la función, pero en este caso el parámetro N sería 3 (por cada llamada se realiza una copia de los parámetros) y así sucesivamente hasta que se evalúe N con valor 1 que finalizará la función y se empezarán a completarse las expresiones en sentido inverso.
Un parámetro formal de una función puede ser de cualquier tipo, incluyendo vectores o registros. Sin embargo, no puede ser un tipo anónimo, es decir, debe declararse antes, por ejemplo:
type
TVectoris
array
(Positiverange
<>)of
Float;function
Suma_Componentes (V: TVector)return
Floatis
Resultado: Float := 0.0;begin
for
Iin
V'Rangeloop
Resultado := Resultado + V(I);end
loop
;return
Resultado;end
Suma_Componentes;
En este ejemplo, se puede utilizar la misma función para cualquier vector de una dimensión, no importa el número de componentes del vector. Así pues, no hay límites estáticos en los parámetros pasados a las funciones. Por ejemplo, se puede utilizar de la siguiente forma:
V4: TVector(1..4) := (1.2, 3.4, 5.6, 7.8); Suma: Float; Suma := Suma_Componentes (V4);
De igual manera, una función también puede devolver un tipo del que no se conocen a priori sus límites. Por ejemplo:
function
Invierte_Componentes (V: TVector)return
TVectoris
Resultado: TVector(V'Range);-- Fija el límite del vector a devolver.
begin
for
Iin
V'Rangeloop
Resultado(I) := V(V'First + V'Last - I);end
loop
;return
Resultado;end
Invierte_Componentes;
La variable Resultado tiene los mismo límites que V, con lo que siempre se devuelve un vector de la misma dimensión que el pasado como parámetro.
Indicar también que una función devuelve un valor que puede utilizarse sin necesidad de realizar una asignación a una variable, con lo que se puede hacer referencia directamente, por ejemplo, a Invierte_Componentes(V4)(1), con lo que se obtendría el primer componente del vector devuelto por la función (en este caso 7.8).
Parámetros nombrados
Destacar, que tanto en procedimientos como en funciones, se puede alterar el orden de los parámetros en la llamada utilizando la notación nombrada, es decir, se especifica en la llamada el nombre del parámetro real seguido del símbolo => y después el parámetro formal. Por ejemplo:
Ecuación_Cuadrática (Válida => OK, A => 1.0, B => 2.0, C => 3.0, R1 => P, R2 => Q); F := Factorial (N => (3 + I));
Esto implica conocer el nombre de los parámetros formales que, en principio, bastaría con mirar la especificación del paquete. Como se asigna uno a uno el parámetro formal al real, no hay ningún problema de ambigüedad. Conviene recordar que el parámetro formal va a la izquierda del símbolo =>, por si se da la situación de que la variable utilizada como parámetro real se llame de igual manera que el formal (no habría ambigüedad ya que están en distinto ámbito).
Parámetros por defecto
Por otra parte, se pueden establecer parámetros formales, tanto en procedimientos como en funciones, que tengan valores por defecto y, por tanto, se pueden obviar en la llamada al subprograma. Por ejemplo:
procedure
Prueba_Por_Defecto (A :in
Integer := 0; B:in
Integer := 0);
Se puede llamar de estas formas:
Prueba_Por_Defecto (5, 7);-- A = 5, B = 7
Prueba_Por_Defecto (5);-- A = 5, B = 0
Prueba_Por_Defecto;-- A = 0, B = 0
Prueba_Por_Defecto (B => 3);-- A = 0, B = 3
Prueba_Por_Defecto (1, B => 2);-- A = 1, B = 2
En la primera sentencia, se utiliza una llamada "normal" (con notación posicional); en la segunda, se utiliza posicional y por defecto; la tercera utiliza todos los parámetros por defecto; en la cuarta nombrada y por defecto; y, por último, la quinta utiliza notación posicional y nombrada.
Manual de referencia de Ada
Sobrecarga
En Ada se puede utilizar un identificador con más de un significado. Esto es lo que se conoce como sobrecarga. Los identificadores que se pueden sobrecargar son:
- Literales enumeración.
- Operadores.
- Subprogramas.
Sobrecarga de literales de enumeración
Se pueden emplear identificadores iguales en distintos tipos enumeración, por ejemplo:
declare
type
TColoris
(ROJO, VERDE, AZUL);type
TLuzis
(ROJO, AMARILLO, VERDE); Pixel: TColor; Indicador: TLuz := VERDE;begin
-- Se emplea calificador para distinguir.
Pixel := TColor'(VERDE);end
;
Sobrecarga de operadores
También se puede utilizar una función para definir una operación y
sobrecargar cualquiera de los operadores ya establecidos excepto /= (que
toma su valor a partir del operador =, pero que puede redefinirse si el valor de retorno no es de tipo Boolean). Por ejemplo, se puede
hacer una función "*"
que multiplique escalarmente dos vectores:
function
"*"
(A, B: Vector)return
Floatis
Resultado: Float := 0.0;begin
for
Iin
A'rangeloop
Resultado := Resultado + A(I) * B(I);end
loop
;return
Resultado;end
"*"
;
Las reglas a cumplir a la hora de sobrecargar un operador son:
- No cambiar la sintaxis.
- No cambiar el número de parámetros de un operador (recuérdese que + y - pueden ser unarios o binarios).
- La precedencia se mantiene.
Por otro lado, una recomendación muy importante es no definir un operador para un nuevo tipo dándole un significado distinto al del operador predefinido. Es decir, + siempre ha de representar conceptos relacionados con la adición; *, multiplicación; and
, conjunción, etc.
Sobrecarga de subprogramas
Un subprograma sobrecargará un significado ya existente siempre que su especificación sea suficientemente diferente, es decir, pueden existir dos subprogramas con el mismo identificador siempre que se distingan por el número o tipo de sus parámetros. Por ejemplo:
declare
type
TAlimentois
(PAN, LECHE, ACEITE);type
TProducto_Automóvilis
(ACEITE, BUJÍA);procedure
Comprar (ProdAlim: TAlimento)is
-- ...
procedure
Comprar (ProdAutom: TProducto_Automóvil)is
-- ...
begin
Comprar (ACEITE);-- Ambigüedad.
Comprar (TAlimento'(ACEITE));-- Se supera la ambigüedad.
Comprar (ProdAlim => ACEITE);-- También se supera la ambigüedad.
end
;
Manual de referencia de Ada
Entrada/salida
Ada tiene cinco bibliotecas predefinidas independientes para operaciones de entrada/salida. Por tanto, la lección más importante a aprender es elegir la más adecuada en cada caso.
Direct I/O
Direct I/O se usa para acceso directo a archivos que contienen únicamente elementos del mismo tipo. Con Direct_IO el cursor del archivo se puede situar en cualquier elemento de ese tipo (el concepto conocido en inglés como random access). El tipo de los elementos ha de ser un subtipo definitivo (definite subtype), es decir, un subtipo cuyos objetos tienen un tamaño definido.
Sequential I/O
Sequential I/O se usa para el acceso secuencial a archivos que únicamente contienen elementos de un tipo especificado.
Con Sequential I/O es posible elegir entre tipos definitivos y no definitivos, pero los elementos se han de leer uno tras otro.
Storage I/O
Storage I/O nos permite almacenar un único elemento en un buffer de memoria. El elemento ha de ser de un subtipo definitivo. Storage I/O se usa en la programación concurrente para trasladar elementos de una tarea a otra.
Stream I/O
Stream I/O es el paquete de entrada/salida más potente de Ada. Stream I/O permite mezclar objetos de diferentes tipos en un archivo secuencial. Para leer/escribir de/en un stream (un flujo de datos) cada tipo provee un atributo 'Read y otro 'Write. Estos atributos están definidos por el compilador para cada tipo que declaremos.
Estos atributos tratan los elementos como datos sin interpretar. Son ideales tanto para entrada/salida de bajo nivel como para interoperar con otros lenguajes de programación.
Los atributos 'Input y 'Output añaden información de control adicional al archivo, tal como el atributo 'First y el 'Last de un array.
En programación orientada a objetos es posible usar los atributos 'Class'Input y 'Class'Output para almacenar y recuperar correctamente un tipo concreto de la misma clase.
Stream I/O es también el paquete de entrada/salida más flexible. Todos los atributos de E/S pueden sobrescribirse con subprogramas definidos por el usuario y es posible definir nuestros propios tipos de Stream I/O usando técnicas avanzadas de orientación a objetos.
Text I/O
Text I/O probablemente sea el tipo de entrada/salida más usada. Todos los datos del archivo se representan en formato de texto legible. Text I/O provee la posibilidad de definir el layout de líneas y páginas, pero el estándar es texto de forma libre.
Biblioteca predefinida
Existen varios paquetes predefinidos para la entrada/salida en Ada:
- Paquete Ada.Direct_IO
- Paquete Ada.Sequential_IO
- Paquete Ada.Storage_IO
- Paquete Ada.Streams
- Paquete Ada.Text_IO, con sus paquetes genéricos anidados: Complex_IO, Decimal_IO, Enumeration_IO, Fixed_IO, Float_IO, Integer_IO y Modular_IO.
- Paquete Ada.Float_Text_IO
- Paquete Ada.Integer_Text_IO
Manual de referencia de Ada
Interfaz con otros lenguajes
En el mercado existen miles de bibliotecas de programación escritas en C ó C++. También es bastante común tener que reutilizar una parte de nuestros programas escritos en otro lenguaje. Para este propósito existen en Ada los pragmas (directivas de compilación) de interfaz con otros lenguajes, definidos en el Annex B: Interface to Other Languages. El estándar define interfaces con los lenguajes C, COBOL y Fortran, y también con ensamblador. Las implementaciones pueden definir interfaces con otros lenguajes siguiendo el mismo esquema. Así, por ejemplo, GNAT proporciona interfaz con C++ (aunque el name mangling suele dificultar esta tarea).
Para importar desde nuestro programa Ada una función definida en otro lenguaje se utiliza el pragma Import. Ésta es la base de los conocidos bindings que nos permiten utilizar una librería escrita en otro lenguaje desde Ada. Para hacer que un subprograma escrito en Ada pueda ser llamado desde otro lenguaje, se utiliza el pragma Export.
También se pueden importar y exportar variables o constantes.
A la hora de enlazar (link) el programa completo tenemos que especificar a nuestro compilador con qué bibliotecas externas ha de enlazar para encontrar los subprogramas u objetos importados. Para esto también puede ser de ayuda el pragma Linker_Options, con el cual se pueden especificar los argumentos para el linker.
La biblioteca predefinida tiene varios paquetes, todos ellos hijos de Interfaces destinados a facilitar la interfaz con otros lenguajes, definiendo tipos y subprogramas para ello:
Ejemplo de importación desde C
-- Ejemplo para limpiar la pantalla
with
Interfaces.C;procedure
Clsis
package
Crenames
Interfaces.C;function
System (Command : C.Char_Array)return
C.Int;pragma
Import(C, System,"system"
); Rc : C.Int;begin
Rc := System (C.To_C ("cls"
));-- Comando sintaxis MS-DOS. En Unix sería clear
end
Cls;
Este ejemplo compilaría perfectamente usando GNAT con:
gnatmake cls.adb
Sin embargo si en vez de importar funciones de las librerías estándar de C tenemos que importar nuestras propias funciones, primero tendríamos que definirlas en su correspondiente archivo .c, compilarlas y luego pasárselas al compilador de Ada.
Por ejemplo si nuestras funciones están definidas en el archivo mis_funciones.c y si usamos el compilador gcc:
gcc -c mis_funciones.c
Con esto se obtiene un mis_funciones.o que se ha de pasar al gnatmake para que enlace con las funciones:
gnatmake ejemplo.adb -largs mis_funciones.o
Si no lo hacemos así fallará la fase de enlace del programa porque existen referencias a funciones externas no encontradas.
Manual de Referencia de Ada
Cláusulas de representación
Una de las características de Ada que facilitan la programación de bajo nivel es la posibilidad de definir a nivel de bit la representación de nuestros tipos. Es posible definir el tamaño y los valores internos de los tipos enumerados, por ejemplo:
type Estado_T is (Desconectado, Conectado, Desconocido);
-- Especificamos que el compilador ha de usar dos bits
for Estado_T'Size use 2;
-- Especificamos los valores que se usarán en memoria para cada valor
for Estado_T use (Desconectado => 0, Conectado => 1, Desconocido => 3);
También es posible definir la estructura en memoria de los registros:
type Estructura_Estado_T is
record
Estado : Estado_T;
Numero_Reintentos : Integer;
end record;
for Estructura_Estado_T use
record
Estado at 0 range 0 .. 2; -- Primer y 2º bit de la 1ª palabra
Numero_Reintentos at 1 range 0 .. 31; -- 32 bits en la 2ª palabra
end record;
Otra manera de controlar la representación es con el pragma Pack, que indica al compilador que use la mínima representación para los componentes de un array o de un registro, sin dejar espacios vacíos entre los elementos.
type Array_Bits_T is array (Positive range <>) of Boolean;
-- Indicamos que se use un único bit para el tipo Boolean (dos valores, un
-- bit es suficiente) y de este modo tenemos un array de bits sobre el
-- que podemos aplicar operadores booleanos:
pragma Pack (Array_Bits_T);
Manual de Referencia de Ada
Diseño y programación de sistemas grandes
Los sistemas empotrados suelen ser grandes y complejos, formados por subsistemas relacionados, pero relativamente independientes. Algunos lenguajes ignoran el hecho de que los programas se construyen por partes, cada una de ellas compilada por separado y todas ellas enlazadas en una aplicación final. El resultado se convierte en aplicaciones monolíticas difíciles de mantener. Otros lenguajes, en contraste, parten del concepto de módulo y proporcionan mecanismos de encapsulamiento y abstracción que ayudan a programar sistemas grandes, ya que el trabajo del equipo de programación y posterior mantenimiento del sistema se ve facilitado. Uno de estos lenguajes es Ada, que está fuertemente fundamentado en la disciplina de la ingeniería del software por lo que es el lenguaje más apropiado en la programación de sistemas empotrados industriales grandes.
Ada asume la necesidad de la compilación separada y proporciona dos mecanismos para realizarla, uno ascendente y otro descendente:
- El mecanismo descendente (descomposición): consiste en dividir un sistema complejo en componentes más sencillos. Es apropiado para el desarrollo de grandes programas coherentes que, son divididos en varias subunidades que pueden compilarse por separado. Las subunidades se compilan después que la unidad de la que forman parte.
- El mecanismo ascendente (abstracción): consiste en la especificación de los aspectos esenciales de un componente, posponiendo su diseño detallado. Es apropiado para la creación de bibliotecas de programa en las que las unidades se escriben para uso general y, consecuentemente, se escriben antes que los programas que las vayan a utilizar.
El diseño de sistemas mediante módulos permite encapsular partes del sistema mediante interfaces bien definidas y permiten utilizar técnicas que facilitan el desarrollo de sistemas grandes como:
- Ocultación de información.
Las unidades de programa en Ada son las siguientes:
- Subprograma: que define los algoritmos ejecutables. Los procedimientos y las funciones son subprogramas.
- Paquete: define una colección de entidades. Los paquetes son el principal mecanismo de agrupación de Ada.
- Tarea: define una computación que puede llevarse a cabo en paralelo con otras computaciones.
- Unidades genéricas: ayudan a realizar código reutilizable. Pueden ser subprogramas o paquetes.
- Unidad protegida: puede coordinar el acceso a datos compartidos en el procesamiento paralelo. Aparece en el estándar Ada 95.
En Ada, las unidades de compilación pueden ser:
- Especificaciones de subprogramas
- Especificaciones de paquetes
- Cuerpos de subprogramas o paquetes
Algunos compiladores pueden establecer ciertos requisitos para las unidades de compilación. Por ejemplo, GNAT en su configuración predefinida exige que cada unidad esté definida en un fichero, con el nombre de la unidad y la extensión .ads para especificaciones y .adb para cuerpos. El guión "-" se ha de utilizar en sustitución del punto "." para unidades hijas y subunidades.
Paquetes
Los paquetes exportan mediante una interfaz bien definida tipos, objetos y operaciones y permiten ocultar su implementación, lo que proporciona al programador tipos abstractos de datos y subprogramas de manera transparente.
Los paquetes de Ada proporcionan:
- Abstracción de datos.
- Encapsulamiento.
- Compilación separada y dependiente.
Especificación y cuerpo
El paquete consta de especificación (parte visible) y cuerpo (implementación que se oculta) y pueden compilarse por separado.
La sintaxis de su especificación es la siguiente:
especificación_paquete ::=package
[ identificador_unidad_padre . ] identificadoris
{ declaración_básica } [private
{ declaración_básica } ]end
[ [ identificador_unidad_padre . ] identificador ] ;
La sintaxis del cuerpo de un paquete es la siguiente:
cuerpo_paquete ::=package
body
[ identificador_unidad_padre . ] identificadoris
[ parte_declarativa ] [begin
secuencia_de_sentencias [exception
manejador_de_excepción { manejador_de_excepción } ] ]end
[ [ identificador_unidad_padre . ] identificador ] ;
Un paquete permite agrupar declaraciones y subprogramas relacionados.
Ejemplos
Por ejemplo, para implementar una pila de enteros:
package
Pila_Enterosis
-- Especificación.procedure
Poner (Elem: Integer); -- Interfaz.function
Quitarreturn
Integer; -- Interfaz.end
Pila_Enteros;
package
body
Pila_Enterosis
-- Cuerpo. Max :constant
:= 100; -- Se ocultan las variables locales. Pila:array
(1..Max)of
Integer; -- Se ocultan las variables locales. Cima: Integerrange
0..Max; -- Se ocultan las variables locales.procedure
Poner (Elem: Integer)is
-- Implementación.begin
Cima := Cima + 1; Pila (Cima) := Elem;end
Poner;function
Quitarreturn
Integeris
-- Implementación.begin
Cima := Cima - 1;return
Pila(Cima + 1);end
Quitar;begin
Cima := 0; -- Inicialización.end
Pila_Enteros;
En este caso, se tiene una interfaz que proporciona acceso a dos subprogramas para manejar la pila, aunque también se podrían haber exportado tanto tipos como objetos, constantes, tareas e incluso otros paquetes. Por ejemplo:
package
Sobre_Diasis
type
TDiais
(LUN, MAR, MIE, JUE, VIE, SAB, DOM);subtype
TDiaLaborableis
TDiarange
LUN..VIE; SiguenteDia:constant
array
(TDia)of
TDia := (MAR, MIE, JUE, VIE, SAB, DOM, LUN);end
Sobre_Días;
En este caso, el paquete no necesitaría cuerpo. Todos los elementos definidos en el paquete son accesibles, por lo que podríamos utilizar Sobre_Dias.TDia
, Sobre_Dias.SAB
, Sobre_Dias.JUE
o Sobre_Dias.TDiaLaborable
.
Dependencia entre especificación y cuerpo
La especificación del paquete y el cuerpo pueden compilarse por separado. Mediante este encapsulamiento, ahora no es posible desde fuera modificar, por ejemplo, el valor de la cima de la pila, pues este objeto no es visible. Así se evita un mal empleo de la pila por alguien que pueda no conocer su implementación.
Si la especificación de un paquete contiene la especificación de un subprograma, entonces, el cuerpo del paquete debe contener el correspondiente cuerpo del subprograma. Sin embargo, pueden existir subprogramas dentro del cuerpo del paquete que no se declaren en su especificación, serían, por tanto, internos.
Destacar que dentro del cuerpo del paquete se inicializa el valor de la cima de la pila (después de begin). Esto sucede cuando se elabora el paquete. Si no necesita ninguna sentencia, se puede omitir el begin.
Declaración y visibilidad
Los paquetes se pueden declarar en cualquier parte declarativa, es decir, en un bloque, subprograma o dentro de otro paquete. En el caso del ejemplo, para utilizar la pila de números enteros, se podría hacer así:
declare
N: Integer;package
Pila_Enterosis
-- ...end
Pila_Enteros;begin
Pila_Enteros.Poner (15); N := Pila_Enteros.Quitar;end
;
Dentro del paquete se puede llamar a Poner o a Pila_Enteros.Poner, pero fuera del paquete únicamente se puede llamar a dicho procedimiento de la forma Pila_Enteros.Poner. Además, las variables Max, Pila y Cima no son visibles.
Importación de paquetes
Para la utilización de los paquetes desde otras unidades de compilación, se definen estas dos cláusulas:
Manual de referencia de Ada
Cláusula use
Definición
Si no se desea tener que escribir siempre Pila_Enteros.Poner para llamar a dicho procedimiento desde fuera del paquete, se puede utilizar la cláusula use, cuya sintaxis es la siguiente:
cláusula_use_paquete ::= use
identificador { , identificador } ;
Así pues, siguiendo con el ejemplo de la sección anterior, se podría escribir:
-- ...
declare
use
Pila_Enteros; N: Integer;begin
Poner (15); N := Quitar;end
;
Dicha cláusula use es semejante a una declaración y su ámbito llega al final del bloque. Incluso podría existir otra cláusula use más interna que se refiera al mismo paquete.
En el caso de existir varios paquetes anidados, se puede utilizar la notación punto para distinguirlos, por ejemplo:
package
P1is
package
P2is
-- ...
end
P2;-- ...
end
P2;
use
P1, P1.P2;-- Ilegal sería "use P2;"
Para utilizar el paquete Standard, que contiene todas las entidades predefinidas, no se necesita cláusula use.
Desventajas
Muchos proyectos prohíben el uso de esta cláusula porque dificulta la comprensión del código y la localización de los tipos o subprogramas importados. Si deseas usarla lo más recomendable es usar un único use por cada unidad y haciéndolo sólo de los paquetes más conocidos o de los predefinidos.
Véase también
Cláusula with
Si una unidad se compila aparte (método usual si se va a utilizar en más programas), se puede hacer referencia a dicha unidad (cuya especificación y cuerpo pueden estar en ficheros distintos) mediante la cláusula with, cuya sintaxis es la siguiente:
cláusula_with ::= with
identificador { , identificador } ;
Así, se podría utilizar el paquete Pila_Enteros del ejemplo anterior de esta manera:
with
Pila_Enteros;-- Se hace visible el paquete para todo el programa.
procedure
Prueba_Pila_Enterosis
use
Pila_Enteros;-- Para no usar nombre del paquete en las llamadas.
N: Integer;begin
Poner (15); N := Quitar;end
Prueba_Pila_Enteros;
La cláusula with tiene que ir antes de la unidad (no puede ir dentro de un ámbito más interno), de este modo, se ve claramente la dependencia entre las unidades.
Si la cláusula use se coloca inmediatamente después de la cláusula with correspondiente, se podrá utilizar los nombres de las entidades de los paquetes sin hacer referencia cada vez al nombre del paquete, en toda la unidad.
Si el paquete P usa los servicios del paquete Q y éste a su vez utiliza
los servicios de R, a no ser que P utilice directamente los servicios de
R, se debería utilizar únicamente la cláusula with Q;
dentro de P. El
programador de Q no tiene porqué conocer el paquete R.
Destacar que las especificaciones de unidades utilizadas con with deben compilarse antes que la unidad que contiene dichas cláusulas with, aunque de esto se suelen encargar automáticamente los compiladores.
También hay que tener en cuenta, que las dependencias que sean únicamente del cuerpo no deben darse en la especificación a no ser que se exporte algo que necesite de dicho paquete.
Hay un paquete que no es necesario que se mencione en la cláusula with, el paquete Standard.
Manual de referencia de Ada
Declaraciones
Una declaración es una construcción del lenguaje que asocia un nombre con una entidad. Ada distingue cuidadosamente entre declaraciones (que introducen nuevos identificadores) y sentencias (que utilizan dichos identificadores). Hay dos clases de declaraciones:
- Implícitas: que ocurren como consecuencia de la semántica de otra construcción.
- Explícitas: aparecen en el texto del programa y pueden ser:
- Declaraciones de tipo, subtipos, variables y constantes (objetos), excepciones, especificaciones de subprogramas o paquetes, cláusulas de representación o cláusulas use.
- Los cuerpos de los subprogramas, paquetes y tareas.
Declaraciones de subprogramas
En ocasiones es necesario utilizar declaraciones de subprogramas, por ejemplo, si se va a utilizar la recursividad entre dos procedimientos:
procedure
P;-- Declaración de P, necesaria para utilizara en Q.
procedure
Qis
-- Cuerpo de Q.
begin
P;-- ...
end
Q;procedure
Pis
-- Repite especificación para declarar el cuerpo.
begin
Q;-- ...
end
P;
También puede resultar útil declarar las especificaciones de los subprogramas en el comienzo del programa a modo de índice, sobre todo si hay muchos cuerpos.
Vista de una entidad
Todas las declaraciones contienen una definición de vista de una entidad. Una vista consiste en:
- Un identificador de la identidad.
- Características específicas de la vista que afectan al uso de la entidad a través de dicha vista.
En la mayoría de los casos, una declaración contiene la definición de la vista y de la entidad misma, pero una declaración de renombrado, sin embargo, no define una entidad nueva, sino que define una nueva vista de una entidad.
Parte declarativa
Una secuencia de declaraciones constituye una parte declarativa. Las siguientes construcciones de Ada tienen asociada una parte declarativa:
- Bloque.
- Cuerpo de un subprograma.
- Cuerpo de un paquete.
- Cuerpo de una tarea.
- Cuerpo de una entrada a un objeto protegido.
Por ejemplo, en un procedimiento, la parte declarativa comprendería las sentencias entre procedure y begin. El cuerpo de un procedimiento puede contener en su parte declarativa el cuerpo de otro procedimiento. Por tanto, los procedimientos pueden ser declarados sin límite de niveles de anidamiento sucesivos.
En ocasiones, las declaraciones requieren una segunda parte (se dice que requieren una terminación). La declaración y su terminación deben tener el mismo nombre y deben ocurrir en la misma región declarativa. Por ejemplo, un paquete necesita un cuerpo (que sería la terminación) si en su parte declarativa contiene alguna declaración que requiere una terminación que no se encuentre en dicha declaración.
Región declarativa de una declaración
Hay que distinguir entre región declarativa de una declaración y parte
declarativa. En el supuesto de una declaración de una variable (por
ejemplo I: Integer := 0;
) se extiende por una región de texto que abarca
sólo una línea, sin embargo, otras declaraciones más complejas como
procedimientos y paquetes constituyen muchas más líneas de texto. En
Ada, existen las siguientes construcciones que tienen asociada una
región declarativa:
- Cualquier declaración que no sea terminación de otra.
- Bloque
- Bucle
- Construcción accept.
- Manejador de excepción.
La región declarativa de cada una de estas construcciones comprende:
- El texto de la construcción misma.
- Texto adicional, determinado de esta manera:
- Si una declaración está incluida en la región, también lo está su terminación.
- Si una unidad de biblioteca está incluida en la región, también lo están sus unidades hijas.
- Si está incluido el requerimiento de compilación separada, también lo está su subunidad correspondiente.
Así, se pueden extraer las siguientes conclusiones:
- La especificación de un paquete y su cuerpo forman parte de la misma región declarativa porque el cuerpo es la terminación de la especificación, por lo que no se puede declarar la misma variable en la especificación y en el cuerpo.
- Ya que la declaración y el cuerpo de un paquete pueden estar en compilaciones distintas, la región declarativa de una declaración de paquete puede abarcar porciones de texto en varias unidades de compilación distintas.
- Todas las unidades de biblioteca son hijas de Standard, luego toda unidad de biblioteca pertenece a su región declarativa.
Manual de referencia de Ada
Ámbito
Cada declaración tiene asociada una porción del texto del programa que se denomina ámbito de la declaración. En dicha porción es el único lugar donde se puede hacer referencia a dicha declaración. El ámbito de una declaración consiste en:
- Si la declaración es una unidad de biblioteca, todos sus dependientes semánticos.
- Si la declaración no es una unidad de biblioteca, una porción de la región declarativa que la encierra inmediatamente. Dicha porción se extiende desde el comienzo de la declaración hasta el final de su región declarativa.
Resumiendo, como ejemplo:
procedure
Pis
-- ///////////////////////////////////// Parte declarativa de P.
-- ...
-- -------------------------------------- Ámbito de A.
A: Float;-- -------------------------------------- Ámbito de Q.
-- ////////////// Región declarativa de I y R.
procedure
Qis
-- /////////////////////////////// Parte declarativa de Q.
-- ------------------------------ Ámbito de I.
I: Integer := 0;-- ...
-- //////// Región declarativa de J.
package
Ris
-- ///////// Parte declarativa pública de R.
-- --------------------- Ámbito de J.
J: Integer := I;-- ...
-- ///// Fin parte declarativa pública de R.
end
R;package
body
Ris
-- ///////// Parte declarativa privada de R.
-- --------------------- Ámbito de K
K: Integer := I + J;-- ...
-- ///// Fin parte declarativa privada de R.
begin
-- ...
end
R;-- ----------------- Fin ámbito de K
-- ----------------- Fin ámbito de J.
-- //// Fin región declarativa de J.
-- /////////////////////////// Fin parte declarativa de Q.
begin
-- ...
end
Q;-- ...
-- -------------------------- Fin ámbito de I.
-- ////////// Fin región declarativa de I y R.
-- ///////////////////////////////// Fin parte declarativa de P.
begin
-- ...
end
P;-- ---------------------------------- Fin ámbito de A.
-- ---------------------------------- Fin ámbito de Q.
Para la cláusula with también se define un ámbito, que consiste en la región declarativa de su declaración si aparece delante de la declaración de una unidad de biblioteca y en el cuerpo si aparece delante de un cuerpo.
Para la cláusula use: si actúa como una declaración, su ámbito es la porción de la región declarativa que empieza justo después de la cláusula y finaliza junto con dicha región declarativa; si actúa como cláusula de contexto, su ámbito es el mismo que el de la cláusula with.
Manual de referencia de Ada
Visibilidad
Una entidad es visible en un punto dado si se puede utilizar su identificador para referirse a ella en dicho punto. La diferencia con el ámbito es que éste es la región de texto donde una determinada entidad es visible.
En el caso de una declaración de una variable, no se puede utilizar el identificador hasta que no se haya terminado su declaración (por ejemplo, sería ilegal la declaración I: Integer := I +1;) ya que no es visible hasta que no se haya terminado de declarar.
Como ejemplo, en el caso de un bloque:
declare
-- Ámbito de I externa.
I, J: Integer;-- Visibilidad de I externa.
begin
-- ...
declare
-- Ámbito de I interna.
I: Integer := 0;-- Visibilidad de I interna, oculta la visibilidad de I externa.
begin
-- ...
end
;-- Fin visibilidad de I interna, oculta la visibilidad de I externa.
-- Fin ámbito de I interna.
end
;-- Fin visibilidad de I externa.
-- Fin ámbito de I externa.
En este caso, la visibilidad de la I externa se ve ocultada cuando se hace visible a la I interna. Sin embargo, si se dota de nombre al bloque, se puede hacer referencia a una variable supuestamente ocultada con la notación punto:
Externo:declare
I, J: Integer;-- I externa.
begin
-- ...
declare
I, K: Integer;-- I interna.
begin
K := J + Externo.I;-- Se hace referencia a la I externa.
end
;end
Externo;
Igualmente se puede hacer con bucles y, por supuesto, con subprogramas y paquetes, pues deben poseer un identificador.
En el caso de una entidad declarada en la parte no privada de la especificación de un paquete, se aplican las mismas reglas dentro del paquete pero, fuera del mismo, la entidad no es visible a menos que se escriba el nombre del paquete mediante la notación punto o, alternativamente, se escriba una cláusula use.
Reglas de visibilidad
Una declaración es directamente visible en un lugar determinado cuando su nombre, sin notación punto, es suficiente para referenciarla. La visibilidad puede ser inmediata o mediante una cláusula use.
Los identificadores visibles en un punto son aquellos visibles antes de considerar ninguna cláusula use más aquellos que se hacen visibles debido a las cláusulas use.
La regla básica es que un identificador de un paquete se hace visible mediante una cláusula use si el mismo identificador no está también en otro paquete con otra cláusula use, por supuesto, siempre que el identificador no sea ya visible. Si no se cumple, hay que recurrir a la notación punto.
Se aplica una regla ligeramente diferente cuando todos los identificadores son subprogramas o literales enumeración. En este caso, un identificador que se hace visible mediante una cláusula use no puede ocultar nunca otro identificador, aunque sí puede sobrecargarlo.
Manual de referencia de Ada
Renombrado
El renombrado o redenominación se utiliza para dar a una entidad un
identificador más conveniente en una determinada porción del programa.
Se suele emplear para resolver ambigüedades y para evitar el uso de la
notación punto. Para ello se emplea la palabra reservada renames
. Por
ejemplo:
function
"*" (X, Y: TVector)return
Floatrenames
ProductoEscalar;
Con ello se consigue utilizar indistintamente tanto "*" como ProductoEscalar (definido con anterioridad) para referirse a la misma función.
También se puede evitar la notación punto sin tener que importar todos los identificadores con la cláusula use:
procedure
Poner (Elem: Integer)renames
PilaEnteros.Poner;
El renombrado se puede utilizar con objetos (variables y constantes), excepciones, subprogramas, y paquetes. No se aplica a tipos, aunque un subtipo que no añade restricciones es equivalente a un renombrado.
F: TFecharenames
Agenda(I).FechaNacimiento; ------package
Prenames
Plantilla_Pila;
Reseñar que el renombrado no corresponde a una sustitución de texto. La identidad del objeto se determina cuando se realiza el renombrado.
Manual de referencia de Ada
La biblioteca
La biblioteca Ada (unidades y subunidades)
La biblioteca Ada es la piedra angular de este lenguaje en la construcción de sistemas grandes, pero fiables.
Los programas grandes deben ser descompuestos en subsistemas, cada uno de ellos con su propia estructura interna. La respuesta a este requerimiento en Ada es la biblioteca Ada y tiene las siguientes características:
- Integrado en el lenguaje.
- Facilita la creación y mantenimiento de un subsistema, actuando como repositorio estructurado de todos sus componentes.
- Ofrece a los programas que hacen uso de un subsistema un interfaz fácil de utilizar y que es selectivo a componentes internos.
Los compiladores de Ada toman el código fuente y la biblioteca referenciada y producen un código objeto y, además, una biblioteca actualizada con dicho código objeto. Es como si la biblioteca Ada "recordara" las compilaciones que se realizan en el sistema. A diferencia de los compiladores de otros lenguajes, que únicamente generan el código objeto sin incorporarlo a ninguna biblioteca.
El concepto de incorporación a la biblioteca no está definido por el lenguaje Ada, sino por el propio compilador. Por ejemplo, en la implementación de Ada de GNU denominada GNAT, la biblioteca se implementa sobre un sistema de ficheros. La compilación de un fichero que contiene, por ejemplo, un procedimiento, produce un fichero objeto y una colección de enlaces al resto de la biblioteca (fichero con la extensión .ali de Ada Library Information), dentro del mismo directorio. El compilador puede tener ahora dos "vistas" diferentes de la biblioteca Ada, una con el procedimiento incorporado y otra sin él.
La estructura formal de un programa Ada es la siguiente:
- Un programa es un conjunto de compilaciones. El concepto de compilación no está especificado por el lenguaje Ada, pero suele ser un fichero fuente.
- Una compilación es una secuencia de unidades de compilación. Por ejemplo, una compilación con seis unidades de compilación puede ser un fichero con cuatro procedimientos y dos paquetes. El número de unidades de compilación en una compilación puede estar limitado por la implementación. Por ejemplo, el compilador GNAT únicamente permite una unidad de compilación por cada compilación.
- Una unidad de compilación puede ser bien una unidad de biblioteca o bien una subunidad.
- Una unidad de biblioteca es la declaración o cuerpo de un procedimiento o de un paquete.
- Una subunidad es una parte de una unidad de biblioteca que se desea separar y compilar por separado.
La biblioteca se alimenta de los programas, que no son más que un conjunto de unidades de compilación que se suman a la biblioteca cuando se compila el programa. Cuando un programa está correctamente construido, se incorpora a la biblioteca. Los programas nuevos utilizan el material compilado ya disponible en la propia biblioteca.
Hay que tener presente que los programas se escriben por partes que son compiladas por separado y luego se enlazan para dar el resultado final. Ada proporciona dos mecanismos para ello:
- Unidades de biblioteca: mecanismo ascendente.
- Subunidades: mecanismo descendente.
Subsecciones
Unidades de biblioteca
Una unidad de biblioteca puede ser una especificación de subprograma o una especificación de paquete; a los cuerpos correspondientes se les denomina unidades secundarias. Se puede compilar especificación y cuerpo juntos, pero es conveniente hacerlo separadamente con el fin de mejorar la accesibilidad y el mantenimiento de los programas.
Cuando se compila una unidad, ésta se almacena dentro de la biblioteca de programas. Una vez que se incluye en la biblioteca, una unidad puede ser usada por cualquier otra unidad que se compile a continuación, esta dependencia se indica con la cláusula with.
Si el cuerpo de un subprograma es por sí mismo suficiente para definir un subprograma completo. Es entonces cuando se le clasifica como una unidad de biblioteca, en vez de tratarlo como una subunidad.
Si la especificación y el cuerpo se compilan por separado, entonces, el cuerpo debe compilarse después de la especificación, es decir, el cuerpo es dependiente de la especificación. Sin embargo, toda unidad que utilice el paquete es dependiente únicamente de la especificación, aspecto destacable de Ada. Con ello, aunque cambie el cuerpo del paquete, si no se cambia la especificación (interfaz con el exterior), no es necesario volver a recompilar las unidades que estaban utilizando dicho paquete. Se puede apreciar que la compilación separada de especificación y cuerpo simplifica el mantenimiento de los programas.
Como es obvio, las unidades de biblioteca no pueden sobrecargarse ni pueden ser operadores.
Manual de referencia de Ada
Unidades hijas
El empleo de unidades hijas surge ante la necesidad de poder referenciar a un gran número de unidades de biblioteca con distintos nombres. Al igual que un sistema de archivos jerarquizado mediante directorios y subdirectorios, la biblioteca Ada contiene una jerarquía en su organización. Las unidades hijas son un paso más allá respecto a las subunidades, pues permiten extender la funcionalidad de un paquete sin modificar el paquete en cuestión.
El padre de todas las unidades de biblioteca es el paquete Standard. De este modo, las unidades de biblioteca creadas se agregan como hijas de Standard y se les denomina unidades de biblioteca raíz. Estas unidades serían hermanas de los paquetes predefinidos Standard.Ada, Standard.System y Standard.Interfaces. Y cada una de ellas puede a su vez contener unidades hijas.
Espacio de nombres
Las unidades hijas forman también un espacio de nombres. Desde dentro de la jerarquía es posible referirse a las entidades definidas en el paquete padre como si estuviesen en la propia unidad. Igualmente al referirse a unidades hermanas.
Por ejemplo:
package
Servidoris
type
Petición_Tis
private
;-- ...
end
Servidor;
package
Servidor.Sesiónis
type
Sesión_Tis
record
Petición : Petición_T;-- Equivalente a Servidor.Petición_T
-- ...
end
record
;-- ...
end
Servidor.Sesión;
Los paquetes se pueden encontrar en cualquier punto de la jerarquía, y los subprogramas sólo en las hojas del árbol.
Visibilidad
La parte privada de un paquete hijo y su cuerpo pueden referenciar las entidades definidas en la parte privada del paquete padre.
Manual de referencia de Ada
Subunidades
El cuerpo de un paquete, subprograma o tarea puede ser "extraído" de la unidad o subunidad de biblioteca que lo engloba y compilarse por separado en lo que viene a denominarse subunidad. En la unidad que lo engloba, el cuerpo "extraído" se sustituye por un "resguardo" del cuerpo. Cualquier unidad de compilación puede tener subunidades.
En un ejemplo anterior, se construía un paquete de una pila de números enteros con dos procedimientos Poner y Quitar, que interesa compilar por separado, luego se escribiría:
package
body
Pila_Enterosis
-- Cuerpo.
Max :constant
:= 100; Pila: array(1..Max)of
Integer; Cima: Integerrange
0..Max;procedure
Poner (Elem: Integer)is
separate
;-- Se compila aparte.
function
Quitarreturn
Integeris
separate
;-- Se compila aparte.
begin
Cima := 0;-- Inicialización.
end
Pila_Enteros;
A los subprogramas que se van a compilar aparte (Poner y Quitar) se les denomina subunidades. Su cuerpo deberá implementarse en otro fichero de esta forma:
separate
(Pila_enteros)-- Indica la unidad de la que se extrajo.
procedure
Poner (Elem: Integer)is
begin
Cima := Cima + 1; Pila (Cima) := Elem;end
Poner;
Y de manera análoga se procedería con Quitar.
En el caso de que R sea subunidad de Q y ésta a su vez de P, que es una unidad de biblioteca, entonces la implementación de R debe comenzar con separate (P.Q).
Una subunidad depende de la unidad de la que fue separada y, por tanto debe compilarse después de ella.
La visibilidad dentro de la subunidad es exactamente igual que si no hubiera sido separada, es decir, por ejemplo, una cláusula with en la unidad principal se aplica a todas sus subunidades.
Si se necesita de una unidad únicamente dentro de una subunidad, a fin
de no complicar las dependencias de compilación, se deberá incluir la
cláusula with justo antes de la declaración subunidad, es decir, delante
de separate (Pila_Enteros)
.
Manual de referencia de Ada
Compilación separada y dependiente
Ada realiza una compilación separada y dependiente.
Una compilación separada significa que el programa principal y un subprograma pueden escribirse por separado en ficheros distintos.
Una compilación dependiente significa que el compilador va a llevar a cabo la comprobación de que los tipos y el número de parámetros de la invocación en el subprograma invocante concuerdan con los tipos y el número de parámetros del subprograma invocado.
En otros lenguajes en los que se realiza una compilación independiente (por ejemplo el lenguaje C), no se advierte que los parámetros de llamada se corresponden y compila correctamente. Esta situación en un sistema de control es intolerable. El fallo no se detecta en la compilación y puede que tampoco en las pruebas.
En Ada, cuando desde una unidad de biblioteca se utiliza un tipo o un subprograma de otra unidad, se puede entender que depende semánticamente de ella.
Cuando una unidad ha sido compilada con éxito, se incorpora a la biblioteca del lenguaje. Así, cuando el compilador encuentra una llamada a un subprograma, contrasta el número y el tipo de los parámetros de la llamada contra la declaración del subprograma invocado, declaración que debe haber sido previamente compilada y que, en consecuencia, debe estar ya en la biblioteca. Por lo tanto, se puede decir que es la biblioteca Ada la que implementa la dependencia.
Ada permite incluso escribir y compilar la subrutina invocante antes que la subrutina invocada de forma consistente. Esto se consigue compilando únicamente la especificación, dejando la compilación del cuerpo para más tarde. En dicha especificación se deja detallado el nombre, el número y los tipos de los parámetros, además de indicar si son de entrada, salida o ambos. Esta es toda la información que necesita el compilador para compilar una llamada a un subprograma. Cuando, posteriormente, se compile el cuerpo del subprograma, se comprobará que es consistente con la especificación.
La forma de expresar que una unidad depende de otra se realiza mediante la cláusula with. Cuando el compilador encuentra dicha cláusula, extrae de la biblioteca el interfaz de la unidad que acompaña a with.
El orden de compilación es el siguiente: una unidad sólo se incorpora a la biblioteca después de que todas las unidades de las que depende se han incorporado también a la biblioteca. Ello implica que:
- Si especificación y cuerpo de una unidad se compilan por separado, es preciso compilar antes la especificación.
- Si la especificación de una unidad es cambiada y, por lo tanto, es recompilada de nuevo, todas las unidades que dependen de ella deben ser recompiladas.
- Si el cuerpo de una unidad se cambia de una forma consistente con su especificación, las unidades que dependen de esta unidad no necesitan ser recompiladas.
El lenguaje Ada viene con varios paquetes predefinidos como Text_IO. Estos paquetes ya han sido incorporados a la biblioteca del lenguaje. Hay, sin embargo, una excepción que es el paquete Standard, que no necesita la cláusula with. Finalmente, todas las unidades incorporadas a la biblioteca Ada deben tener nombres diferentes. En otro caso, se produce el reemplazamiento de la unidad residente por la nueva unidad con el mismo nombre.
Notese que cuando se dice que una unidad se ha de compilar antes que otra no quiere decir que el programador se tenga que preocupar de estos temas, pues los entornos de desarrollo de Ada vienen acompañados de herramientas de compilación que se encargan de recompilar todas las unidades necesarias y sólo las que han quedado obsoletas por un cambio en el código fuente. Por ejemplo, con el compilador GNAT, esta herramienta es gnatmake.
Manual de referencia de Ada
Tipos abstractos de datos
Tipos abstractos de datos (tipos privados)
Una de las principales contribuciones de los lenguajes de alto nivel es que el programador no tiene que preocuparse de cómo se representan físicamente los datos en el computador. De esta idea surge el concepto de tipo de datos. Una extensión del mismo es el tipo abstracto de datos. Su implementación es de nuevo desconocida para el programador, esta vez no porque desconozca la arquitectura del computador subyacente, sino porque es encapsulado en un módulo que no permite el acceso directo a los detalles de su implementación. En su lugar, se proporciona al programador operaciones sobre el tipo que son invocaciones a entradas del módulo que lo encapsula.
Por ejemplo, consideremos la utilización de un tipo abstracto de datos que represente a un número complejo:
package
Números_complejosis
type
TComplejois
record
Real, Imag: Float;end
record
; I:constant
TComplejo := (0.0, 1.0);function
"+" (X, Y: TComplejo)return
TComplejo;function
"-" (X, Y: TComplejo)return
TComplejo;function
"*" (X, Y: TComplejo)return
TComplejo;function
"/" (X, Y: TComplejo)return
TComplejo;end
Números_complejos;
De este modo, el usuario debe conocer los detalles de la implementación y sabe que se utiliza una representación cartesiana. Además, el usuario está obligado a hacer uso de la representación.
Para impedir el uso del conocimiento de la representación con vistas,
por ejemplo, a poder cambiar ésta posteriormente, se puede hacer uso de
los tipos privados definiéndolos mediante la palabra reservada private
:
package
Números_complejosis
-- Parte visible.
type
TComplejois
private
;-- Tipo privado.
I:constant
TComplejo;-- No se puede asignar valor todavía.
function
"+" (X, Y: TComplejo)return
TComplejo;function
"-" (X, Y: TComplejo)return
TComplejo;function
"*" (X, Y: TComplejo)return
TComplejo;function
"/" (X, Y: TComplejo)return
TComplejo;function
Construir_complejo (R, I: Float)return
TComplejo;function
Parte_imaginaria (X: TComplejo)return
Float;function
Parte_real (X: TComplejo)return
Float;private
-- Parte oculta.
type
TComplejois
record
Real, Imag: Float;end
record
; I:constant
TComplejo := (0.0, 1.0);end
Números_complejos;
Ahora, se ha definido TComplejo como tipo privado y se resguardan los
detalles de su implementación en la parte no visible del paquete después
de la palabra reservada private
y hasta el fin de la especificación del
paquete. En la parte visible (desde el comienzo de la especificación
hasta private), se da la información disponible fuera del paquete.
Las únicas operaciones disponibles son la asignación, la igualdad y la desigualdad, aparte de las añadidas en el paquete.
Nótese que el valor de I no se puede dar pues no se conocen todavía los detalles de la implementación, se declara como constante y se le asigna después un valor en la parte privada.
Las funciones Construir_complejo, Parte_imaginaria y Parte_real son ahora necesarias pues el usuario ya no conoce la estructura del tipo TComplejo y se necesita realizar dicha interfaz para poder manejar objetos del tipo privado.
El cuerpo se podría implementar de la siguiente manera:
package
body
Números_complejosis
function
"+" (X, Y: Tcomplejo)return
TComplejois
begin
return
(X.Real + Y.Real, X.Imag + Y.Imag);end
"+";-- ... "-", "* y "/" similarmente.
function
Construir_complejo (R, I: Float)return
TComplejois
begin
return
(R, I);end
Construir_complejo;function
Parte_real (X: TComplejo)return
Floatis
begin
return
X.Real;end
Parte_real;-- ... Parte_imaginaria análogamente.
end
Números_complejos;
Y podría ser utilizado transparentemente, por ejemplo, dentro de un bloque como:
declare
use
Números_complejos; C1, C2: TComplejo; R1, R2: Float;begin
C1 := Construir_complejo (1.5, -6.0); C2 := C1 + I; R := Parte_real (C2) + 8.0;end
;
Si ahora se quisiera cambiar la implementación del tipo TComplejo y representarlo en forma polar, no sería necesario cambiar la parte visible de la especificación, por lo que todas las unidades que utilicen dicho paquete no tienen la necesidad de actualizarse. La interfaz exportada no ha cambiado y, por tanto, los programas que la utilizarán pueden seguir haciéndolo. Por ejemplo, ahora se podría representar en la parte privada de la especificación del paquete como:
-- ...
private
Pi:constant
:= 3.1416;type
TComplejois
record
R: Float; Theta: Floatrange
0.0 .. 2*Pi;end
recod; I:constant
TComplejo := (1.0, 0.5*Pi);end
Números_complejos;
Lo único que se necesitaría sería reescribir el cuerpo del paquete y recompilarlo.
Enlaces externos
Manual de referencia de Ada
Tipos limitados
Tipos privados limitados
Cuando se define un tipo privado, se predefinen inherentemente las operaciones de asignación, igualdad y desigualdad. Si no se quiere que exista ninguna operación, sino únicamente las definidas en el paquete, se debe emplear el tipo privado limitado.
Como consecuencia de no tener operador de asignación, la declaración de un objeto de dicho tipo no puede incluir un valor inicial. Esto también tiene la consecuencia de que no pueden existir constantes de un tipo privado limitado.
La ventaja es que el programador de la unidad que contenga un tipo privado limitado se asegura el control absoluto sobre los objetos de dicho tipo.
Para indicarlo, se define el tipo como limited
private
. Por ejemplo,
implementado un tipo abstracto de datos pila:
package
Pilasis
type
TPilais
limited
private
;-- Tipo privado limitado.
procedure
Poner (P:in
out
TPila; X:in
Integer);procedure
Quitar (P:in
out
TPila; X:out
Integer);function
"=" (P1, P2: TPila)return
Boolean;private
Max:constant
:= 100;type
TVectorEnterosis
array
(Integerrange
<>)of
Integer;type
TPilais
record
P: TVectorEnteros(1..Max); Cima; Integerrange
0..Max := 0;end
record
;end
Pilas;
La función "=" se implementa para comprobar que dos pilas tienen el mismo número de elementos y cada uno de ellos en el mismo orden deber ser iguales. Por eso, se ha optado por un tipo privado limitado.
Manual de referencia de Ada
Unidades genéricas
Polimorfismo paramétrico
La idea de reutilización de código surge ante la necesidad de construir programas en base a componentes bien establecidos que pueden ser combinados para formar un sistema más amplio y complejo. La reutilización de componentes mejora la productividad y la calidad del software. El lenguaje Ada soporta esta característica mediante las unidades genéricas.
Una unidad genérica es aquella en la que se manipulan tipos que posteriormente instanciará el usuario, es decir, se utiliza a modo de plantilla. Se pueden hacer unidades genéricas de subprogramas y paquetes. Sintácticamente se podría describir como:
unidad_genérica ::=generic
{ lista_parámetros_genéricos } ( especificación_subprograma | especficicación_paquete ) lista_parámetros_genéricos ::= identificador { , identificador } [in
[out
] ] tipo [ := expresión ] ; |type
identificadoris
( (<>) |range
<> |digits
<> |delta
<> | definición_vector | definición_puntero ) | declaración_privada_de_tipo | declaración_formal_procedimiento | declaración_formal_paquete
Por ejemplo, para reutilizar un procedimiento de intercambio de variables:
generic
type
TElementois
private
; -- Parámetro tipo formal genérico.procedure
Intercambiar (X, Y:in
out
TElemento);
procedure
Intercambiar (X, Y:in
out
TElemento)is
Temporal : TElemento;begin
Temporal := X; X := Y; Y := Temporal;end
Intercambiar;
La especificación del subprograma va precedida por la parte formal genérica, que consta de la palabra reservada generic seguida por una lista de parámetros formales genéricos que puede ser vacía.
El subprograma Intercambiar es genérico y se comporta como una plantilla. Hay que destacar que las entidades declaradas como genéricas no son locales, por ello es necesario instanciarlas. Por ello, para poder utilizar la unidad del ejemplo es necesario crear una instancia suya para el tipo que se quiera usar, su sintaxis sería:
instanciación_unidad_genérica ::= (package
|procedure
|function
) identificadoris
new
identificador [ ( parámetro_instanciado { , parámetro_instanciado } ) ] ;
Por ejemplo:
procedure
Intercambiar_enterosis
new
Intercambiar (Integer);
Con ello, se puede utilizar el procedimiento para tipos Integer, si se quiere utilizar para cualquier otro tipo basta con volver a instanciarlo para el nuevo tipo con otro nombre o, si se utiliza el mismo identificador en la instanciación, se sobrecarga el procedimiento y puede ser utilizado para distintos tipos:
procedure
Interis
new
Intercambiar (Float);procedure
Interis
new
Intercambiar (TDía);procedure
Interis
new
Intercambiar (TElemento => TPila);
De igual modo, se pueden emplear paquetes genéricos, por ejemplo, para implementar una plantilla del tipo abstracto de datos pila:
generic
-- Especificación unidad genérica. Max: Positive; -- Parámetro bjeto formal genérico.type
TElementois
private
; -- Parámetro tipo formal genérico.package
Plantilla_pilais
procedure
Poner (E: TElemento);function
Quitarreturn
TElemento;end
Plantilla_pila;
package
body
Plantilla_pilais
-- Cuerpo unidad genérica. Pila: array(1..Max)of
TElemento; Cima: Integerrange
0..Max; -- ...end
Plantilla_pila;
Ahora se podría utilizar una pila de un tamaño y tipo determinados, para ello, habría que crear un ejemplar, por ejemplo, de esta manera:
declare
package
Pila_reales_de_100is
new
Plantilla_pila (100, Float);use
Pila_reales_de_100;begin
Poner (45.8); -- ...end
;
En la segunda línea del código anterior se ha creado una instancia del paquete Plantilla_pila
que se llama Pila_reales_de_100
. A partir de ese momento se pueden acceder a los miembros del paquete, ya que la creación de la instancia implica su visibilidad (igual que si se ejecutara la sentencia
).
with
Pila_reales_de_100;
Parámetros de unidades genéricas
Resaltar que los objetos declarados como parámetros formales son de
modo in
por defecto y pueden ser in
o in
out
, pero nunca out
. En el caso
de que sea in
, se comportará como una constante cuyo valor lo
proporciona el parámetro real correspondiente. Como resulta obvio, un
parámetro genérico in
no puede ser de un tipo limitado, pues no se
permite la asignación y el parámetro formal toma su valor mediante
asignación. En el caso de que el parámetro genérico sea de modo in
out
,
se comporta como una variable que renombra al parámetro real
correspondiente; en este caso, el parámetro real debe ser el nombre de
una variable y su determinación se realiza en el momento de la creación
del ejemplar.
Además de parámetros genéricos de tipo y objetos, se pueden incluir parámetros formales de subprogramas o paquetes, por ejemplo:
generic
type
TElemis
private
;with
function
"*" (X, Y: TElem)return
TElem;function
cuadrado (X : TElem)return
TElem;
function
cuadrado (X: TElem)return
TElemis
begin
return
X * X; -- El operador "*" formal.end
cuadrado;
Se utilizaría, por ejemplo, con matrices (teniendo previamente definida la operación de multiplicación de matrices), de la siguiente manera:
with
Cuadrado;with
Matrices;procedure
Prueba_operacionesis
function
Cuadrado_matrizis
new
Cuadrado (TElem => Matrices.TMatriz, "*" => Matrices.Producto_matrices); A: TMatriz := TMatriz.Identidad;begin
A := Cuadrado_matriz (A);end
Prueba_operaciones;
Los tipos formales de un genérico se pueden especificar para que pertenezcan a una determinada clase de tipos.
Tipo formal | Instanciable con |
---|---|
|
Cualquier tipo con operador de igualdad y asignación definidos |
|
Cualquier tipo con discriminante de tipo TD |
|
Cualquier tipo con cualquier discriminante |
|
Cualquier tipo (sea limitado o no) |
|
Cualquier tipo discreto |
|
Cualquier tipo entero con signo |
|
Cualquier tipo de coma fija |
|
Cualquier tipo de coma flotante |
|
Cualquier tipo array con índice I y tipo de elementos E (I y E podrían ser a su vez otros parámetros formales) |
|
Cualquier tipo puntero que apunte a objetos de tipo O (O podría ser a su vez otro parámetro formal) |
En el cuerpo sólo podemos hacer uso de las propiedades de la clase de tipo del parámetro real. Es decir, a diferencia de las plantillas de C++, la especificación del genérico es un contrato que ha de cumplir la implementación.
Ver también
Manual de referencia de Ada
Excepciones
En Ada, cuando se produce algún error durante la ejecución de un programa, se eleva una excepción. Dicha excepción puede provocar la terminación abrupta del programa, pero se puede controlar y realizar las acciones pertinentes. También se pueden definir nuevas excepciones que indiquen distintos tipos de error.
Excepciones predefinidas
En Ada, dentro del paquete Standard, existen unas excepciones predefinidas, éstas son:
- Constraint_Error
- cuando se intenta violar una restricción impuesta en una declaración, tal como indexar más allá de los límites de un array o asignar a una variable un valor fuera del rango de su subtipo.
- Program_Error
- se produce cuando se intenta violar la estructura de control, como cuando una función termina sin devolver un valor.
- Storage_Error
- es elevada cuando se requiere más memoria de la disponible.
- Tasking_Error
- cuando hay errores en la comunicación y manejo de tareas.
- Numeric_Error
- en Ada 83 se podía presentar cuando ocurría un error aritmético. A partir del estándar Ada 95, desaparece por motivos de portabilidad y pasa a ser un renombrado de Constraint_Error. Por ejemplo, en Ada 83 al dividir entre cero podía saltar Constraint_Error o Numeric_Error (dependiendo del compilador). En Ada 95 este error siempre levanta Constraint_Error.
- Name_Error
- se produce cuando se intenta abrir un fichero que no existe.
Manejador de excepciones
Cuando se espere que pueda presentarse alguna excepción en parte del código del programa, se puede escribir un manejador de excepciones en las construcciones que lo permitan (bloques o cuerpos de subprogramas, paquetes o tareas), aunque siempre está el recurso de incluir un bloque en cualquier lugar del código.
Su sintaxis sería:
manejador_excepción ::=when
[ identificador : ] elección_excepción { | elección_excepción } => secuencia_sentencias elección_excepción ::= identificador |others
A la sentencia que comienza por when
, se le denomina manejador de excepción.
La palabra reservada others
indica cualquier otra excepción y debe ser la única y última opción. Por ejemplo, en un bloque:
begin
-- ...
exception
when
Constraint_Error => Put ("Error de rango.");when
Program_Error | Tasking_Error => Put ("Error de flujo.");when
others
=> Put ("Otro error.");end
;
En el momento en el que se produzca la elevación de Constraint_Error
durante la ejecución de la secuencia de sentencias entre begin
y
exception
, el flujo de control se interrumpe y se
transfiere a la secuencia de sentencias que siguen a la palabra
reservada =>
del manejador correspondiente.
Otro ejemplo con una función:
function
Mañana (Hoy: TDía)return
TDíais
begin
return
TDía'Succ(Hoy);exception
when
Constraint_Error =>return
TDía'First;end
Mañana;
Nótese que no se puede devolver nunca el control a la unidad donde se elevó la excepción. Cuando se termina la secuencia de sentencias del manejador, termina también la ejecución de dicha unidad.
Si no se controla una excepción, ésta se propaga dinámicamente por las sucesivas unidades invocantes hasta que se maneje en otra o directamente termina la ejecución del programa proporcionando un mensaje con la excepción provocada por pantalla.
Declaración y elevación de excepciones
Normalmente, es probable prever una situación de error que no se encuentra entre las excepciones predefinidas, por ello, se puede declarar excepciones. Por ejemplo:
Error: exception
;
Con lo que se puede elevar dicha excepción en el momento pertinente mediante la sentencia raise, cuya sintaxis es:
elevación_excepción ::= raise
[ identificador ] ;
Por ejemplo, en un paquete de manejo de una pila estática de números enteros:
package
Pila_enterosis
ErrorPilaEnteros:exception
;procedure
Poner (X: Integer);function
Quitarreturn
Integer;end
Pila_enteros;package
body
Pila_enterosis
Max:constant
:= 100; Pila:array
(1..Max)of
Integer; Cima: Integerrange
0..Max;procedure
Poner (X: Integer)is
begin
if
Cima = Maxthen
raise
ErrorPilaEnteros;-- Se eleva la excepción.
end
if
; Cima := Cima + 1; P(Cima) := X;end
Poner;function
Quitarreturn
Integeris
begin
if
Cima = 0then
raise
ErrorPilaEnteros;-- Se eleva la excepción.
end
if
; Cima := Cima - 1;return
Pila(Cima+1);end
Quitar;begin
Cima := 0;end
Pila_enteros;
Obsérvese que no hace falta else en la sentencias if, pues al elevar la excepción, finaliza la ejecución del subprograma.
Ahora se podría escribir:
declare
use
Pila_enteros;begin
Poner (5);-- ...
exception
when
ErrorPilaEnteros =>-- ... Manipulación incorrecta de la pila.
when
others
=>-- ...
end
;
Si se quiere que dicha excepción no se propague más allá de la unidad en la que se elevó pero no se quiere manejar, se puede emplear una única sentencia vacía (null) dentro de su manejador correspondiente:
procedure
Vaciar_pila_enterosis
Basura: Integer;use
Pila_enteros;begin
loop
Basura := Quitar;end
loop
;exception
when
ErrorPilaEnteros =>null
;end
Vaciar_pila_enteros;
Aunque esto no evitaría que se terminara la ejecución de la unidad.
En el caso en el que se quiera propagar una excepción después de haber ejecutado las sentencias pertinentes, se incluiría una sentencia raise dentro del manejador:
-- ...
exception
when
ErrorPilaEnteros => Put ("Pila utilizada incorrectamente."); Vaciar_pila_enteros;raise
ErrorProcesamieto;-- Se propaga otra excepción.
end
;
En este caso se propaga otra excepción, pero podría haber sido la misma simplemente con raise, sin crear una nueva ocurrencia de la excepción, por ejemplo:
-- ...
exception
when
FalloEnVálvula => Put ("Se ha producido un fallo en la válvula.");raise
;-- Se propaga la misma excepción del manejador.
end
;
Así, se puede realizar un manejo de la excepción en varias capas, realizando sucesivas acciones en cada una de ellas. Dicha sentencia raise sin argumentos debe ser invocada directamente en el manejador, no es posible invocarla en un procedimiento llamado por el manejador.
Información de la excepción
Ada proporciona información sobre una determinada excepción haciendo uso del paquete predefinido Ada.Exceptions y tras obtener la ocurrencia de la excepción mediante esta notación:
when
Ocurrencia : ErrorSensor =>
Put_Line (Ada.Exceptions.Exception_Information (Ocurrencia));
Manual de referencia de Ada
Enlaces externos
- Excepciones: artículo de la Universidad de Valladolid. Ejemplos en Ada y Eiffel.
Concurrencia
Concurrencia
Para más información, véase el artículo «Programación concurrente» en Wikipedia. |
La concurrencia es la simultaneidad de hechos. Un programa concurrente es aquel en el que ciertas unidades de ejecución internamente secuenciales (procesos o threads), se ejecutan paralela o simultáneamente.
Existen 3 formas básicas de interacción entre procesos concurrentes:
- Sincronización (p.e. las citas o paso de mensajes).
- Señalización (p.e. los semáforos).
- Comunicación (p.e. uso de memoria compartida).
La concurrencia o procesamiento paralelo se ha implementado en leguajes de programación de distinta manera:
- Programación concurrente clásica: se basa en la utilización de variables compartidas. Es el caso de Modula-2 o Concurrent Pascal. Para ello, se emplean herramientas como semáforos, regiones críticas y monitores.
- Programación concurrente distribuida: se basa en la transferencia de mensajes entre los procesos o threads. Es el caso de C/POSIX, Occam o Ada. Se emplean herramientas como canales, buzones y llamadas a procedimiento remoto.
En Ada se emplea una programación concurrente distribuida y la principal forma de sincronizar las unidades de ejecución, conocidas como tareas, son los puntos de entrada a la tarea o citas.
Subsecciones
- Tareas
- Sincronización de tareas mediante puntos de entrada o citas (entry)
- Aceptación de citas (accept)
- Selección de citas (select)
- Llamadas a punto de entrada complejas
- Tareas dinámicas: creación dinámica de tareas (tipos tareas)
Tareas
Definición de tareas
En Ada, a la unidad de proceso secuencial que puede ser ejecutada paralelamente se le denomina task. Es la representación explícita de un proceso (o tarea).
Como en otras construcciones, la tarea de Ada presenta una especificación (interfaz con el exterior) y un cuerpo (descripción del comportamiento dinámico).
La sintaxis de la especificación de una tarea es:
especificación_tarea ::=task
identificador [is
{ punto_entrada_tarea | cláusula_representación } [private
{ punto_entrada_tarea | cláusula_representación } ]end
[ identificador ] ] ; punto_entrada_tarea ::=entry
identificador [ ( tipo | rango ) ] [ ( parámetro { , parámetro } ) ] ;
La sintaxis del cuerpo de una tarea es:
cuerpo_tarea ::=task
body
identificadoris
[ parte_declarativa ]begin
secuencia_de_sentenciasend
[ identificador ] ;
Por ejemplo, dos procesos que escriben un texto por pantalla:
with
Ada.Text_IO;use
Ada.Text_IO;procedure
Tareas_tontasis
task
Tarea1;task
Tarea2;task
body
Tarea1is
begin
loop
Put ("Soy la tarea 1."
);end
loop
;end
Tarea1;task
body
Tarea2is
begin
loop
Put ("Soy la tarea 2."
);end
loop
;end
Tarea2;begin
-- En este momento comienzan a ejecutarse ambas tareas.
Put ("Soy el procedimiento principal."
);end
Tareas_tontas;
En este caso, el orden de los mensajes que aparecen en pantalla es totalmente impredecible, depende del sistema en el que se ejecuten. Ni siquiera es predecible si las frases que vayan apareciendo serán completas o serán truncadas, esto es debido a que el sistema puede decidir suspender la ejecución de alguna tarea en cualquier instante de tiempo y la sentencia Put no es atómica. Ambas tareas comenzarán su ejecución simultáneamente (al menos lógicamente) justo después del begin del procedimiento.
Ciclo de vida y tipos
Hay dos tipos de tareas, según su ciclo de vida:
- Tareas estáticas:
- Dependen del bloque donde se declaran.
- Se activan justo antes de que se ejecuten las instrucciones del bloque donde fueron declaradas.
- Terminan cuando todas sus tareas hijas terminen y llegue a su última instrucción.
- Tareas dinámicas (mediante punteros):
- Dependen del bloque donde se define el tipo puntero.
- Se activan con la sentencia
new
. - Las tareas hijas pueden seguir existiendo cuando se termine su progenitora.
Existen atributos asociados a las tareas, entre otros:
- Tarea'Callable indica falso si la tarea está completada o terminada, en otro caso (ejecutándose o suspendida), verdadero.
- Tarea'Terminated indica verdadero si la tarea ha terminado.
Indicar tan sólo, que una tarea se puede terminar abruptamente mediante
la sentencia abort
y para redirigir una llamada a un punto de entrada
que esté encolada hacia otra cola, se emplea la sentencia requeue
.
Ejemplo
Para un sistema de control de presión y temperatura, se puede realizar el control de ambas magnitudes simultáneamente:
with
Paquete_control_temperatura;use
Paquete_control_temperatura;with
Paquete_control_presión;use
Paquete_control_presión;procedure
Control_temperatura_y_presióntask
Control_temperatura;task
Control_presión;task
body
Control_temperaturais
Temperatura: TTemperatura; Temperatura_referencia: TTemperatura := 55; AcciónCalefactor: TAcciónCalefactor;begin
loop
Temperatura := Leer; AccciónCalefactor := Control (Temperatura, Temp_referencia); Escribir (AcciónCalefactor);delay
(2.0);end
loop
;end
Control_temperatura;task
body
Control_presiónis
Presión: TPresión; Presión_referencia: TPresión := 315; PosiciónValvula: TPosiciónVálvula;begin
loop
Presión := Leer; PosiciónVálvula := Control (Presión, Presión_referencia); Escribir (PosiciónVálvula);delay
(3.0);end
loop
;end
Control_presión;begin
-- Comienzan a ejecutarse Control_temperatura y Control_presión;
null
;-- Un cuerpo de procedimiento no puede estar vacío.
end
Control_temperatura_y_presión;
En el preciso instante en el que comienza a ejecutarse el procedimiento, hay tres procesos ejecutándose simultáneamente, el procedimiento y las dos tareas.
Manual de referencia de Ada
Sincronización mediante citas
Sincronización de tareas mediante puntos de entrada o citas (entry)
Frecuentemente, las tareas interaccionan entre sí y necesitan un mecanismo para comunicarse y sincronizarse, este mecanismo que ofrece Ada se conoce como la cita (rendezvous) o punto de entrada a la tarea.La cita entre dos tareas se produce como consecuencia de la llamada de una tarea a un punto de entrada declarado en otra tarea.
Los puntos de entrada se declaran en la especificación de la tarea, por ejemplo:
task
Tareais
entry
Entrada (N: Integer);end
Tarea;
Un punto de entrada se asemeja a un procedimiento. Los parámetros que admiten son de modo in, out o in out, por defecto, se sobreentiende in. Para invocar a un punto de entrada, se procede de igual manera que en un procedimiento, por ejemplo:
T: Tarea;
-- ...
T.Entrada (8);
Nótese que se debe emplear la notación punto siempre que se realice la llamada fuera de la misma tarea pues una tarea no puede aparecer en una cláusula use. Realizar una llamada a un punto de entrada propio desde la misma tarea está permitido sintácticamente, pero resulta ilógico, pues produciría un interbloqueo consigo misma.
También se pueden definir varios puntos de entrada simultáneamente, por ejemplo:
type
TNivelis
Integerrange
1..10;task
Controladoris
-- Se define un punto de entrada por cada nivel.
entry
Aviso (TNivel) (Elem: TElemento);end
Controlador;
Y se podría llamar a un punto de entrada de los 10 definidos como, por ejemplo:
ProcesoColtrol: Controlador;
-- ...
ProcesoControl.Aviso (3) (1773);
Con ello, se llama al punto de entrada Aviso con el nivel 3 y parámetro 1173.
Las acciones que se llevan a cabo al aceptar una cita se especifican mediante la sentencia accept
, tal y como se explica en el apartado siguiente.
Manual de referencia de Ada
Aceptación de citas
Aceptación de citas (accept)
La forma de aceptar una cita y ejecutar las sentencias que se deseen
es mediante la sentencia accept
, dentro del cuerpo de la tarea que
acepta la cita. Cada sentencia entry
debe corresponderse con una sentencia accept
.
La sintaxis de accept
es:
acceptación_cita ::=accept
identificador [ ( expresión ) ] [ ( especificación_parámetro { ; especificación parámetro } ) ] [do
secuencia_de_sentenciasend
[ identificador ] ] ;
Por ejemplo:
accept
Entrada (N: Integer)do
-- ... Secuencia de sentencias.
end
Entrada;
Se deben repetir los parámetros formales declarados en el punto de entrada de la especificación de la tarea.
La diferencias fundamentales entre los puntos de entrada y los procedimientos son:
- El código existente dentro en la sentencia accept es ejecutado por la tarea propietaria y no por la parte invocante, como en los procedimientos.
- Además, hasta que la tarea no llegue a la ejecución de dicha sentencia
accept
, no puede ser invocado el punto de entrada. De igual manera, la parte invocante queda suspendida hasta que termine la ejecución de la sentenciaaccept
. Éste es el fundamento de la cita.
La forma más simple de sincronizar una tarea que dependa de la terminación de otro código es por ejemplo:
task
Simpleis
entry
Continuar;end
Simple;task
body
Simpleis
begin
-- ...
accept
Continuar;-- Se queda bloqueado hasta que se cite.
-- ...
end
Simple;
Como otro ejemplo, si se quiere implementar una tarea que realice un control de escritura y lectura sobre un buffer de un único elemento:
task
Buffer1is
entry
Escribir (Elem: TElemento);entry
Leer (Elem:out
TElemento);end
Buffer1;task
body
Buffer1is
ElemLocal: TElemento;begin
loop
accept
Escribir (Elem: TElemento)do
ElemLocal:= Elem;-- Guarda el elemento.
end
Escribir; Ada.Text_IO.Put_Line("Elemento escrito, voy a intentar LEER!");accept
Leer (Elem:out
TElemento)do
Elem := ElemLocal;-- Devuelve el elemento.
end
Leer; Ada.Text_IO.Put_Line("Elemento leido, vuelvo a intentar ESCRIBIR");end
loop
;end
Buffer1;
Se aceptan llamadas Buffer1.Escribir(…)
y Buffer1.Leer(…)
de forma
consecutiva, sin posibilidad de escribir o leer dos o más veces
seguidas.
Varias tareas diferentes pueden invocar a los puntos de
entrada y, por tanto, pueden quedar encoladas. Cada punto de entrada
tiene una cola de tareas que esperan llamar a dicho punto de entrada. El
atributo Escribir'Count
contiene el número de tareas que se encuentran
encoladas a la espera de que se ejecute el punto de entrada Escribir
,
pero sólo se puede utilizar dentro de la tarea que contiene el punto de
entrada. Con la ejecución de la sentencia accept
se extraería la
primera tarea de la cola (la primera que llegó).
Por tanto, el ejemplo anterior funciona de la siguiente manera: la tarea Buffer1
llega al accept
de Escribir
y se queda bloqueada allí hasta que otra tarea realice una llamada Buffer1.Escribir(…)
. En ese momento, la tarea Buffer1
ejecuta Escribir
y llega al accept
de Leer
, donde se queda bloqueada hasta que otra tarea realice una llamada Buffer1.Leer(…)
. Se ejecuta Leer
y la tarea Buffer1
vuelve al accept
de Escribir
, y así constantemente. Evidentemente, si hay tareas encoladas en los puntos de entrada de Escribir
o de Leer
, la tarea Buffer1
no se queda bloqueada, sino que atiende a la primera tarea llamante de la cola.
Se puede introducir código entre dos bloques de accept
, tal y como se ve en el ejemplo anterior: cuando se acaba el primer bloque accept
(Escribir
) se ejecuta dicho código y después se entra en la cola de espera del segundo bloque accept
(Leer
).
Si hay definidos varios puntos de entrada simultáneamente, se puede
aceptar uno de ellos, por ejemplo, como:
accept
Aviso (3) (Elem: Telemento)do
-- ...
end
Aviso;
Manual de referencia de Ada
Selección de citas
Selección de citas (select)
Uno de los usos de la sentencia select es permitir a una tarea seleccionar entre varias posibles citas; en este caso, se permite su uso únicamente dentro del cuerpo de una tarea. Su sintaxis es la siguiente:
seleccción_aceptación_cita ::=select
[when
condición => ] ( acceptación_cita | (delay
[until
] expresión ) [ secuencia_de_sentencias ] ) | (terminate
; ) ) {or
[when
condición => ] ( acceptación_cita | (delay
[until
] expresión ) [ secuencia_de_sentencias ] ) | (terminate
; ) ) } [else
sequencia_sentencias ]end
select
;
Esta alternativa de sentencia select
permite una combinación de espera y
selección entre varias aceptaciones de puntos de entrada a la tarea
alternativas. Además, la selección puede depender de condiciones
asociadas a cada alternativa.
La sentencia delay
sirve para indicar que, si en un determinado
intervalo de tiempo no se produce ninguna llamada que corresponda con las
selecciones anteriores, se ejecuten las sentencias posteriores.
La sentencia terminate
se elige en la sentencia select
si la unidad de
la que la tarea depende ha llegado al final y todas las tareas hermanas
y dependientes han terminado. Es una terminación controlada. Esta
alternativa no puede aparecer si hay una alternativa delay
o else
.
Por ejemplo:
task
Servidoris
entry
Trabajar;entry
Cerrar;end
;task
body
Servidoris
begin
loop
select
accept
Trabajardo
-- Se acepta la llamada a trabajar.
Trabajando := True;-- Variable global.
end
; Trabajo_servidor;-- Trabaja.
or
accept
Cerrar;-- Se cierra el servidor.
exit
;or
delay
(60.0);-- ¿Se han olvidado del servidor?
Put ("Estoy esperando trabajar.");-- Otra opción en vez de delay:
-- or
-- --Terminación normal cuando se destruya el objeto tarea.
-- terminate;
end
select
;end
loop
;end
Servidor;
Como otro ejemplo, para garantizar la exclusión mutua a una variable (acceso seguro a memoria compartida), se podría implementar con tareas de la siguiente manera:
task
Variable_protegidais
entry
Leer (Elem:out
TElemento);entry
Escribir (Elem: TElemento);end
;task
body
Variable_protegidais
ElemLocal: TElemento;begin
accept
Escribir (Elem: TElemento)do
ElemLocal := Elem;end
Escribir;loop
select
accept
Escribir (Elem: TElemento)do
ElemLocal := Elem;end
Escribir;or
accept
Leer (Elem:out
TElemento)do
Elem := ElemLocal;end
Leer;end
select
;end
loop
;end
Variable_protegida;
La primera sentencia de la tarea es un accept
de Escribir, con lo que se asegura que la primera llamada le de un
valor a la variable local. En el supuesto de que se realizara una llamada a Leer, ésta quedaría encolada
hasta que se produjera la aceptación de Escribir. Después, la tarea
entra en el bucle infinito que contiene una sentencia select
. Es ahí
donde se acepta tanto llamadas a Escribir como a Leer de la siguiente
manera:
Si no se llama ni a Leer ni a Escribir, entonces la tarea se queda
suspendida hasta que se llame a algún punto de entrada, en ese momento
se ejecutará la sentencia accept
correspondiente.
Si hay una o más llamadas en la cola de Leer, pero no hay llamadas en la de Escribir, se acepta la primera llamada a Leer, y viceversa.
Si hay llamadas tanto en la cola de Leer como en la de Escribir, se hace una elección arbitraria.
Es una tarea que sirve a dos colas de clientes que esperan servicios diferentes. Sin embargo, se impide el acceso múltiple a la variable local.
Manual de referencia de Ada
Llamadas a punto de entrada complejas
Llamadas a punto de entrada complejas
A veces, interesa que una llamada a un punto de entrada de una tarea cumpla unos requisitos. Esto es debido a que se puede bloquear el proceso que realiza la llamada y puede ser interesante disponer de métodos para desbloquearlo si no se cumplen unas determinadas condiciones.
La sentencia select, además de servir como selección de aceptaciones de puntos de entrada dentro del cuerpo de la tarea que los contiene, también proporciona mecanismos para seleccionar el comportamiento de las llamadas a puntos de entrada. Su sintaxis es la siguiente:
llamada_a_punto_de_entrada_compleja ::= llamada_a_punto_de_entrada_con_tiempo_límite | llamada_a_punto_de_entrada_condicional | llamada_a_punto_de_entrada_asíncrona llamada_a_punto_de_entrada_con_tiempo_límite ::=select
identif_p_entrada [ ( tipo | rango ) ] [ ( parámetro { , parámetro } ) ] ; [ secuencia_de_sentencias ]or
delay
[until
] expresión ; [ secuencia_de_sentencias ]end
select
; llamada_a_punto_de_entrada_condicional ::=select
identif_p_entrada [ ( tipo | rango ) ] [ ( parámetro { , parámetro } ) ] ; [ secuencia_de_sentencias ]else
secuencia_de_sentenciasend
select
; llamada_a_punto_de_entrada_asíncrona ::=select
( identif_p_entrada [ ( tipo | rango ) ] [ ( parámetro { , parámetro } ) ] ; ) | (delay
[until
] expresión ; ) [ secuencia_de_sentencias ]then
abort
secuencia_de_sentenciasend
select
;
Tipos de punto de entrada
Como puede apreciarse, hay tres posibles llamadas a puntos de entrada a parte de la simple, éstas son: llamada con tiempo límite, llamada condicional y transferencia asíncrona.
Llamada con tiempo límite
Llama a un punto de entrada que es cancelado si no se produce la aceptación antes de que finalice un plazo de tiempo. Ejemplo:
select
Controlador.Petición (Medio) (Elem);or
delay
50.0; Put ("Controlador demasiado ocupado.");end
select
;
Llamada condicional
Llama a un punto de entrada que es cancelada si no es aceptada inmediatamente, es decir, tiene un tiempo límite nulo. Ejemplo:
select
Procesado.Aviso;else
raise
Error;end
select
;
Transferencia asíncrona
Proporciona la transferencia asíncrona de control cuando se acepte la llamada a un punto de entrada o se cumpla un plazo de tiempo, mientras se esté ejecutando una secuencia de sentencias. Es decir, si se acepta la llamada al punto de entrada o cumple el plazo, se abortan las sentencias que se estuvieran ejecutando. Ejemplo:
select
delay
5.0;raise
FunciónNoConverge;then
abort
Función_recursiva (X, Y);end
select
;
Manual de referencia de Ada
Tareas dinámicas
Creación dinámica de tareas (tipos tareas)
Además de poder declarar tareas como un simple objeto, se pueden declarar un tipo como tipo tarea. Con ello se consigue poder utilizar otros objetos que utilicen dicho tipo tarea como por ejemplo, un vector de tareas o un puntero a tarea (con lo que se consigue crear tareas dinámicamente).
La sintaxis de los tipos tarea es la siguiente:
declaración_tipo_tarea ::=task
type
identificador [ ( discriminante { ; discriminante } ) ] [is
{ punto_entrada_tarea | cláusula_representación } [private
{ punto_entrada_tarea | cláusula_representación } ]end
[ identificador ] ] ; discriminante ::= identificador { , identificador } : [access
] subtipo [ := expresión ]
Con ello, se define un nuevo tipo. Esta definición necesita una terminación, es decir, faltaría declarar el cuerpo de la tarea, que se realiza de igual manera que si se hubiera declarado la tarea simplemente.
Los tipos tarea son privados limitados. Es decir, un objeto declarado de tipo tarea no es una variable, se comporta como una constante. Por tanto, no se permite la asignación, igualdad y desigualdad para los tipos tarea.
Según la sintaxis descrita, se puede definir una tarea como un tipo, por ejemplo, de esta manera:
declare
task
type
TTareaA;task
type
TTareaB;task
type
TTareasMúltiples;type
TVectorTareasis
array
(1..10)of
TTareasMúltiples; A: TTareaA; B: TTareaB; V: TVectorTareas;task
body
TTareaAis
-- ...
end
;task
body
TTareaBis
-- ...
end
;task
body
TTareasMúltiplesis
-- ...
end
;begin
-- A partir de aquí se ejecutan las 12 tareas concurrentemente.
-- ...
end
;
En el momento en el que dé comienzo la ejecución del bloque, justo
después de begin
, dará comienzo la ejecución simultánea de las tareas
definidas en las distintas variables del tipo task
. En este caso
TTareaA, TTareaB y 10 TTareasMúltiples. Pero esta situación es estática,
no se pueden lanzar tareas en un determinado instante; para ello, se
pueden emplear punteros como, por ejemplo:
procedure
Ejemplo_tareas_dinámicasis
task
type
TTarea;type
PTTareais
access
TTarea; T1: PTTarea; T2: PTTarea :=new
TTarea;-- Se crea la tarea T2.all.
begin
T1 :=new
TTarea;-- Se crea la tarea T1.all.
T1 :=null
;-- Se pierde la referencia, pero se sigue ejecutando.
-- ...
end
Ejemplo_tareas_dinámicas;
Las tareas creadas con new
siguen unas reglas de activación y
dependencia ligeramente diferentes. Estas tareas inician su activación
inmediatamente después de la evaluación del asignador de la sentencia
new
. Además, estas tareas no dependen de la unidad donde se crearon,
sino que dependen del bloque, cuerpo de subprograma o cuerpo de tarea
que contenga la declaración del tipo access
en sí. Para referenciar a
tareas dinámicas se emplea el nombre de la variable puntero seguido de
.all
, por ejemplo, T1.all
. Si se quiere que termine una tarea creada dinámicamente se debe utilizar la sentencia abort
. Por ejemplo, abort
T1.all
.
También se pueden crear varios ejemplares de un mismo tipo dependiendo de un parámetro denominado discriminante. Por ejemplo:
task
type
TManejadorTeclado (ID: TIDentifTeclado := IDPorDefecto)is
entry
Leer (C:out
Character);entry
Escribir (C:in
Character);end
TManejadorTeclado;type
PTManejadorTecladois
access
TManejadorTeclado; Terminal: PTManejadorTeclado :=new
TManejadorTeclado (104);
Manual de referencia de Ada
Dependencia de tareas
Dependencia de tareas
Las reglas de dependencia de las tareas son:
- Si la tarea es creada por la elaboración de una declaración de objeto, depende de la unidad que incluya dicha elaboración.
- Si la tarea es creada por la evaluación de una sentencia
new
para un tipo puntero dado, depende de cada unidad que incluya la elaboración de la declaración de dicho tipo puntero.
Por ejemplo:
declare
task
type
TTarea;type
PTTareaGlobalis
access
TTarea; T1, T2: TTarea; PunteroTareaGlobal1: PTTareaGlobal;begin
-- Se activan T1 y T2.
declare
type
PTTareaLocalis
access
TTarea; PunteroTareaGlobal2: PTTareaGlobal :=new
TTarea;-- Se activa PunteroTareaGlobal2.all después de la asignación new.
PunteroTareaLocal: PTTareaLocal :=new
TTarea;-- Se activa PunteroTareaLocal.all después de la asignación new.
T3: TTarea;begin
-- Se activa T3.
-- ...
end
;-- Se espera la terminación de T3 y PunteroTareaLocal.all.
-- Continúa la ejecución de PunteroTareaGlobal2.all.
-- ...
end
;-- Se espera la terminación de T1, T2, PunteroTareaGlobal1.all
-- y PunteroTareaGlobal2.all.
Manual de referencia de Ada
Ejemplos de tareas
Ejemplos completos de tareas
Semáforos
Una posible implementación del tipo abstracto semáforo es con tareas Ada. Pero este ejemplo no se ha de tomar muy en serio, puesto que es un típico caso de inversión de la abstracción, es decir, se hace uso de un mecanismo de alto nivel, las tareas, para implementar uno de bajo nivel, los semáforos. En Ada 95 la mejor manera de implementar un semáforo es un objeto protegido. Sin embargo a efectos didácticos es un buen ejemplo.
generic
ValorInicial: Natural := 1; -- Parám. genérico con valor por defecto.
package Semaforos is
type TSemaforo is limited private;
procedure Wait (Sem: in out TSemaforo);
procedure Signal (Sem: in out TSemaforo);
private
task type TSemaforo is
entry Wait;
entry Signal;
end TSemaforo;
end Semaforos;
package body Semaforos is
procedure Wait (Sem: in out TSemaforo) is
begin
Sem.Wait; -- Llamada a punto de entrada de la tarea.
end Wait;
procedure Signal (Sem: in out TSemaforo) is
begin
Sem.Signal; -- Llamada a punto de entrada de la tarea.
end Signal;
task body TSemaforo is
S: Natural := ValorInicial; -- Es el contador del semáforo.
begin
loop
select
when S > 0 =>
accept Wait;
S := S - 1;
or
accept Signal;
S := S + 1;
or
terminate;
end select;
end loop;
end TSemaforo;
end Semaforos;
with Semaforos;
procedure Prueba_Semaforos is
package Paquete_Semaforos is new Semaforos;
use Paquete_Semaforos;
Semaforo: TSemaforo;
begin -- Aquí se inicia la tarea de tipo TSemaforo (objeto Semaforo).
-- ...
Wait (Semaforo);
-- ...
Signal (Semaforo);
-- ...
end Prueba_Semaforos;
Simulación de trenes
Solución propuesta:
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Numerics.Float_Random;
with Semaforos;
procedure Simulador_Trenes is
Num_Estaciones : constant := 5;
Num_Trenes : constant := 3;
type Num_Estación is range 1 .. Num_Estaciones;
type Num_Tren is range 1 .. Num_Trenes;
package Num_Estación_IO is new Ada.Text_IO.Integer_IO (Num_Estación);
use Num_Estación_IO;
package Num_Tren_IO is new Ada.Text_IO.Integer_IO (Num_Tren);
use Num_Tren_IO;
package Semaforos_Inicial_1 is new
Semaforos (Valorinicial => 1);
use Semaforos_Inicial_1;
Semaforos_Estaciones : array (Num_Estación) of TSemaforo;
task type Tren is
entry Comenzar (Tu_Num : in Num_Tren);
end Tren;
Lista_Trenes : array (Num_Tren) of Tren;
task body Tren is
Mi_Num: Num_Tren;
procedure Pon_Nombre is
begin
Put ("Tren nº"); Put (Mi_Num); Put (": ");
end Pon_Nombre;
Espera_En_Estación: constant Duration := 5.0;
Duración_Mínima: constant Duration := 2.0;
Factor_Duración: constant Duration := 10.0;
Azar_Gen: Ada.Numerics.Float_Random.Generator;
Actual, Siguiente: Num_Estación;
begin
Ada.Numerics.Float_Random.Reset (Azar_Gen);
accept Comenzar (Tu_Num : in Num_Tren) do
Mi_Num := Tu_Num;
end Comenzar;
Pon_Nombre;
Put_Line ("Comienzo el trayecto");
Actual := 1;
loop
Pon_Nombre; Put ("En estación "); Put (Actual); New_Line;
delay Espera_En_Estación;
if Actual = Num_Estaciones then
Siguiente := 1;
else
Siguiente := Actual + 1;
end if;
Wait (Semaforos_Estaciones (Siguiente));
Pon_Nombre;
Put ("Trayecto hacia estación ");
Put (Siguiente);
New_Line;
Signal (Semaforos_Estaciones (Actual));
delay Duration (Ada.Numerics.Float_Random.Random (Azar_Gen))
* Factor_Duración + Duración_Mínima;
Actual := Siguiente;
end loop;
end Tren;
begin
for I in Lista_Trenes'Range loop
Lista_Trenes (I).Comenzar (Tu_Num => I);
end loop;
end Simulador_Trenes;
Buffer circular
Otro ejemplo, una posible implementación de un buffer circular:
generic
type TElemento is private;
Tamaño: Positive := 32;
package Buffer_servidor is
type TBuffer is limited private;
procedure EscribirBuf (B: in out TBuffer; E: TElemento);
procedure LeerBuf (B: in out TBuffer; E: out TElemento);
private
task type TBuffer is
entry Escribir (E: TElemento);
entry Leer (E: out TElemento);
end TBuffer;
end Buffer_servidor;
package body Buffer_servidor is
task body TBuffer is
subtype TCardinalBuffer is Natural range 0 .. Tamaño;
subtype TRangoBuffer is TCardinalBuffer range 0 .. Tamaño - 1;
Buf: array (TRangoBuffer) of TElemento;
Cima, Base: TRangoBuffer := 0;
NumElementos: TCardinalBuffer := 0;
begin
loop
select
when NumElementos < Tamaño =>
accept Escribir (E: TElemento) do
Buf(Cima) := E;
end Escribir;
Cima := TRangoBuffer(Integer(Cima + 1) mod Tamaño);
NumElementos := NumElementos + 1;
or
when NumElementos > 0 =>
accept Leer (E: out TElemento) do
E := Buf(Base);
end Leer;
Base := TRangoBuffer(Integer(Base + 1) mod Tamaño);
NumElementos := NumElementos - 1;
or
terminate;
end select;
end loop;
end TBuffer;
procedure EscribirBuf (B: in out TBuffer; E: TElemento) is
begin
B.Escribir (E);
end EscribirBuf;
procedure LeerBuf (B: in out TBuffer; E: out TElemento) is
begin
B.Leer (E);
end LeerBuf;
end Buffer_servidor;
with Text_IO, Buffer_servidor;
use Text_IO;
procedure Buffer is
Clave_Salida : constant String := "Salir";
type TMensaje is
record
NumOrden: Positive;
Contenido: String (1..20);
end record;
package Cola_mensajes is new Buffer_servidor (TElemento => TMensaje);
use Cola_mensajes;
Cola: TBuffer;
task Emisor;
task Receptor;
task body Emisor is
M: TMensaje := (NumOrden => 1, Contenido => (others => ' '));
Último: Natural;
begin
loop
Put ("[Emisor] Mensaje: ");
Get_Line (M.Contenido, Último);
M.Contenido (Último + 1 .. M.Contenido'Last) := (others => ' ');
EscribirBuf (Cola, M);
M.NumOrden := M.NumOrden + 1;
exit when M.Contenido(Clave_Salida'range) = Clave_Salida;
end loop;
end Emisor;
task body Receptor is
package Ent_IO is new Text_IO.Integer_IO(Integer);
use Ent_IO;
M: TMensaje;
begin
loop
LeerBuf (Cola, M);
exit when M.Contenido(Clave_Salida'range) = Clave_Salida;
Put ("[Receptor] Mensaje número ");
Put (M.NumOrden);
Put (": ");
Put (M.Contenido);
New_Line;
end loop;
end Receptor;
begin
null;
end Buffer;
Problema del barbero durmiente
Esta es una solución al problema del barbero durmiente.
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Numerics.Discrete_Random;
procedure Barberia is
type Rango_Demora is range 1 .. 30;
type Duracion_Afeitado is range 5 .. 10;
type Nombre_Cliente is (Jose, Juan, Iñaki, Antonio, Camilo);
package Demora_Al_Azar is new Ada.Numerics.Discrete_Random
(Rango_Demora);
package Afeitado_Al_Azar is new Ada.Numerics.Discrete_Random
(Duracion_Afeitado);
task Barbero is
entry Afeitar (Cliente : in Nombre_Cliente);
end Barbero;
task type Cliente is
entry Comenzar (Nombre : in Nombre_Cliente);
end Cliente;
Lista_Clientes : array (Nombre_Cliente) of Cliente;
task body Barbero is
Generador : Afeitado_Al_Azar.Generator;
Espera_Máxima_Por_Cliente : constant Duration := 30.0;
begin
Afeitado_Al_Azar.Reset (Generador);
Put_Line ("Barbero: Abro la barbería.");
loop
Put_Line ("Barbero: Miro si hay cliente.");
select
accept Afeitar (Cliente : in Nombre_Cliente) do
Put_Line ("Barbero: Afeitando a " & Nombre_Cliente'Image
(Cliente));
delay Duration (Afeitado_Al_Azar.Random (Generador));
Put_Line ("Barbero: Termino con " & Nombre_Cliente'Image
(Cliente));
end Afeitar;
or
delay Espera_Máxima_Por_Cliente;
Put_Line ("Barbero: Parece que ya no viene nadie,"
& " cierro la barbería.");
exit;
end select;
end loop;
end Barbero;
task body Cliente is
Generador : Demora_Al_Azar.Generator;
Mi_Nombre : Nombre_Cliente;
begin
accept Comenzar (Nombre : in Nombre_Cliente) do
Mi_Nombre := Nombre;
end Comenzar;
Demora_Al_Azar.Reset (Gen => Generador,
Initiator => Nombre_Cliente'Pos (Mi_Nombre));
delay Duration (Demora_Al_Azar.Random (Generador));
Put_Line (Nombre_Cliente'Image (Mi_Nombre) &
": Entro en la barbería.");
Barbero.Afeitar (Cliente => Mi_Nombre);
Put_Line (Nombre_Cliente'Image (Mi_Nombre) &
": Estoy afeitado, me marcho.");
end Cliente;
begin
for I in Lista_Clientes'Range loop
Lista_Clientes (I).Comenzar (Nombre => I);
end loop;
end Barberia;
Problema de los filósofos cenando
Una solución con tareas y objetos protegidos del conocido problema de los filósofos cenando.
package Cubiertos is
type Cubierto is limited private;
procedure Coger(C: in out Cubierto);
procedure Soltar(C: in out Cubierto);
private
type Status is (LIBRE, OCUPADO);
protected type Cubierto(Estado_Cubierto: Status := LIBRE) is
entry Coger;
entry Soltar;
private
Estado: Status := Estado_Cubierto;
end Cubierto;
end Cubiertos;
package body Cubiertos is
procedure Coger (C: in out Cubierto) is
begin
C.Coger;
end Coger;
procedure Soltar (C: in out Cubierto) is
begin
C.Soltar;
end Soltar;
protected body Cubierto is
entry Coger when Estado = LIBRE is
begin
Estado := OCUPADO;
end Coger;
entry Soltar when Estado = OCUPADO is
begin
Estado := LIBRE;
end Soltar;
end Cubierto;
end Cubiertos;
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
with Cubiertos; use Cubiertos;
procedure Problema_Filosofos is
type PCubierto is access Cubierto;
task type TFilosofo(Id: Character; Cubierto1: PCubierto; Cubierto2: PCubierto);
task body TFilosofo is
procedure Comer is
begin
Coger(Cubierto1.all);
Coger(Cubierto2.all);
for i in 1..10 loop
Put(Id & "c ");
delay 1.0;
end loop;
Soltar(Cubierto2.all);
Soltar(Cubierto1.all);
end Comer;
Procedure Pensar is
begin
for i in 1..10 loop
Put(Id & "p ");
delay 1.0;
end loop;
end Pensar;
begin
loop
Comer;
Pensar;
end loop;
end TFilosofo;
Num_Cubiertos: Positive;
begin
Put("Introduce el numero de cubiertos: "); Get(Num_Cubiertos); New_line;
declare
type PTFilosofo is access TFilosofo;
P: PTFilosofo;
C: Character := 'A';
Cuberteria: array (1..Num_Cubiertos) of PCubierto;
begin
for i in 1..Num_Cubiertos loop
Cuberteria(i) := new Cubierto;
end loop;
for i in 1..Num_Cubiertos-1 loop
P := new TFilosofo(C, Cuberteria(i), Cuberteria(i+1));
C := Character'Succ(C);
end loop;
P := new TFilosofo(C, Cuberteria(1), Cuberteria(Num_Cubiertos));
end;
end Problema_Filosofos;
Para evitar el bloqueo mutuo es totalmente imprescindible que al último filósofo se le asignen los cubiertos en ese orden. Sí se hiciese al contrario, el bloqueo no tardaría en aparecer (sobre todo si se eliminan las instrucciones delay):
P := new TFilosofo(C, Cuberteria(Num_Cubiertos), Cuberteria(1));
Chinos: una implementación concurrente en Ada
-- Chinos2: Otra implementación concurrente en Ada
-- Tomás Javier Robles Prado
-- tjavier@usuarios.retecal.es
-- Uso: ./chinos <numero_jugadores>
-- El juego consiste en jugar sucesivas partidas a los chinos. Si un
-- jugador acierta, no paga y queda excluido de las siguientes
-- rondas. El último que quede paga los vinos
-- Copyright (C) 2003 T. Javier Robles Prado
--
-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Numerics.Discrete_Random;
with Ada.Command_Line;
with Ada.Strings.Unbounded;
with Ada.Exceptions;
procedure Chinos is
-- Número Máximo Jugadores que pueden participar (aforo máximo del bar)
MAX : constant Natural := 20;
-- Posibles mensajes que recibe un jugador tras una partida
type Estados is (NO_SIGUES_JUGANDO, SIGUES_JUGANDO, HAS_PERDIDO);
-- Subtipo que modela el número de jugadores posibles
subtype NumMaxJugadores is Natural range 0..MAX;
-- Modela la máxima apuesta que puede darse
subtype MAX_APUESTA is Natural range 0..3*MAX;
-- Nombres posibles para los jugadores. El 0 se utilizará para
-- controlar el caso de que no haya ganador en una partida
subtype TNombre is Integer range -1..MAX;
-- Paquete para Numeros aleatorios:
package Integer_Random is new Ada.Numerics.Discrete_Random(MAX_APUESTA);
-- Apuesta de cada Jugador
Subtype TApuesta is Integer range -1..3*MAX;
-- Mano de cada jugador
subtype TMano is Natural range 0..3;
-- Ficha de cada jugador que guardara el arbitro
type TFicha is record
Nombre : TNombre;
Apuesta : TApuesta := -1;
Mano : TMano;
SigueJugando : Boolean;
end record;
-- Array de Fichas
type TTablon is array(1..MAX) of TFicha;
-- Se define el tipo jugador
task type Jugador;
task Arbitro is
-- El árbitro controla las partidas y sincroniza a los jugadores
entry FijaNumeroJugadores (Num : in NumMaxJugadores);
-- Recoge el argumento de la línea de comandos para saber
-- cuántos jugadores van a participar
entry AsignaNombre (Nombre: out TNombre; NumJug: out NumMaxJugadores);
-- Asigna Nombres (de 1 a NumerosJugadores) a los jugadores que
-- van a participar. A los que no, les asigna un -1 como
-- indicación de que finalicen.
entry SiguesJugando
(Nombre: in TNombre;
JugadorSigueJugando : out Estados;
HuboGanador : out boolean);
-- Mensaje que envía el árbitro a cada jugador tras una
-- partida, comunicándole si ha ganado y deja de jugar, si
-- sigue jugando o si ha perdido y tiene que pagar
entry EnviaApuesta (Nombre: in TNombre ; Apuesta: in TApuesta);
-- El árbitro recibe la apuesta de un jugador
entry ConfirmaApuesta (Confirmada : out Boolean);
-- Respuesta del árbitro sobre si la apuesta es válida (no la
-- ha hecho otro antes)
entry ReEnviaApuesta (Apuesta: in TApuesta);
-- Si la apuesta no es válida se reenvia hasta que lo sea
entry EnviaMano (Nombre: in TNombre ; Mano: in TMano);
-- El jugador envía el número de manos que saca al árbitro
end Arbitro;
task body Arbitro is
-- Funciones y Procedimientos
function NumeroJugadores return NumMaxJugadores is
-- Devuelve el número de jugadores
begin
return 5;
end NumeroJugadores;
function EsApuestaValida (Apuesta: in TApuesta; Tablon: in TTablon)
return Boolean is
-- Devuelve verdadero si la apuesta no ha sido realizada
-- antes por algún otro jugador
Valida : Boolean := True ;
I : TNombre := 1;
begin
for I in 1..MAX loop
if Tablon(I).SigueJugando then
if Tablon(I).Apuesta = Apuesta then
-- Ya está dicha, la apuesta NO es válida
Valida := False ;
end if;
end if;
end loop;
return Valida;
end EsApuestaValida;
function ResultadoGanador (Tablon: in TTablon) return TApuesta is
-- Devuelve el número de monedas que sacaron los jugadores
Suma : TApuesta := 0 ;
begin
for I in 1..MAX loop
if Tablon(I).SigueJugando then
Suma := Suma + Tablon(I).Mano ;
end if;
end loop;
return Suma;
end ResultadoGanador;
procedure ImprimeGanador (Tablon: in TTablon) is
-- Imprimer el nombre del ganador
I : TNombre := 1 ;
Resultado : TApuesta ;
Terminar : Boolean := False;
begin
Resultado := ResultadoGanador(Tablon);
while not Terminar loop
if Tablon(I).Apuesta = Resultado and Tablon(I).SigueJugando then
Put_Line("Ha Ganado el Jugador " & I'Img);
Terminar := True ;
else
if I = MAX then
Put_Line("No ha habido Ganador");
Terminar := True;
else
I := I + 1;
end if;
end if;
end loop;
end ImprimeGanador;
function JugadorEliminado (Tablon: in TTablon) return NumMaxJugadores is
-- Devuelve el jugador que cuya apuesta sea la correcta
Resultado : TApuesta;
Ganador : NumMaxJugadores := 0;
begin
Resultado := ResultadoGanador(Tablon);
for I in 1..MAX loop
if Tablon(I).SigueJugando then
if Resultado = Tablon(I).Apuesta then
Ganador := I ;
end if;
end if;
end loop;
return Ganador;
end JugadorEliminado;
procedure ImprimeTablon(Tablon: in TTablon) is
-- Imprime las apuestas y monedas de los jugadores
begin
for I in 1..MAX loop
if Tablon(I).SigueJugando then
Put_Line("Nombre =" & Tablon(I).Nombre'Img &
" | Apuesta =" & Tablon(I).Apuesta'Img &
" | Mano =" &Tablon(I).Mano'Img );
end if;
end loop;
Put_Line
("Resultado ganador: " & ResultadoGanador(Tablon)'Img);
end ImprimeTablon;
procedure SeparaPartidas (NumPar :in Natural) is
-- Un simple separador para aumentar la claridad
begin
New_Line;
Put_Line("******************************************");
Put_Line("Partida número " & NumPar'Img);
Put_Line("******************************************");
end SeparaPartidas;
-- Variables
-- Número de jugadores de la partida
N : NumMaxJugadores;
Permitidos : NumMaxJugadores;
-- Partida Actual
PartidaActual : NumMaxJugadores;
-- Tablón
Tablon : TTablon;
NombreActual : NumMaxJugadores;
ApuestaValida : Boolean;
Ganador : NumMaxJugadores;
NumeroPartida : Natural;
begin
-- Averigua número de jugadores
accept FijaNumeroJugadores (Num : in NumMaxJugadores) do
N := Num;
end FijaNumeroJugadores;
-- Nombra solo a aquellos que vayan a jugar, a los que no, los
-- nombra como -1
Permitidos := N;
for I in 1..MAX loop
accept AsignaNombre
(Nombre: out TNombre ; NumJug: out NumMaxJugadores) do
if Permitidos > 0 then
Nombre := I;
NumJug := N;
Tablon(I).Nombre := I ;
Tablon(I).SigueJugando := True;
Permitidos := Permitidos - 1;
else
Nombre := -1;
Tablon(I).Nombre := -1;
Tablon(I).SigueJugando := False;
end if;
end AsignaNombre;
end loop;
NumeroPartida := 1;
while N /= 1 loop
-- Para separar las diferentes partidas
SeparaPartidas(NumeroPartida);
-- Recibe las apuestas de cada jugador
for I in 1..N loop
accept EnviaApuesta (Nombre: in TNombre; Apuesta: in TApuesta) do
NombreActual := Nombre;
ApuestaValida := EsApuestaValida(Apuesta,Tablon);
if ApuestaValida then
Tablon(Nombre).Apuesta := Apuesta ;
end if;
end EnviaApuesta;
-- La Apuesta es Válida, se confirma y a otra cosa
if ApuestaValida then
accept ConfirmaApuesta(Confirmada: out Boolean) do
Confirmada := True;
end ConfirmaApuesta;
else
-- La apuesta no es válida. Se comunica esto al jugador para
-- que envíe una nueva apuesta
accept ConfirmaApuesta(Confirmada: out Boolean) do
Confirmada := False;
end ConfirmaApuesta;
while not ApuestaValida loop
-- Aceptará diferentes apuestas hasta q sea válida.
accept ReEnviaApuesta (Apuesta: in TApuesta) do
if EsApuestaValida(Apuesta,Tablon) then
ApuestaValida := True;
Tablon(NombreActual).Apuesta := Apuesta ;
end if;
end ReEnviaApuesta;
accept ConfirmaApuesta(Confirmada: out Boolean) do
Confirmada := ApuestaValida;
end ConfirmaApuesta;
end loop;
end if;
end loop;
-- Recibe lo q saca cada jugador
for I in 1..N loop
accept EnviaMano(Nombre: in TNombre; Mano: in TMano) do
Tablon(Nombre).Mano := Mano ;
end EnviaMano;
end loop;
-- ImprimeResultados de la partida
ImprimeTablon(Tablon);
ImprimeGanador(Tablon);
-- Envía a cada jugador su nuevo estado
Ganador := JugadorEliminado (Tablon);
if Ganador = 0 then
-- Nadie acertó
for I in 1..N loop
accept SiguesJugando
(Nombre: in TNombre;
JugadorSigueJugando : out Estados;
HuboGanador : out boolean) do
JugadorSigueJugando := SIGUES_JUGANDO;
Tablon(Nombre).SigueJugando := True;
HuboGanador := false ;
end SiguesJugando;
end loop;
else
-- Hay ganador
for I in 1..N loop
accept SiguesJugando
(Nombre: in TNombre;
JugadorSigueJugando : out Estados;
HuboGanador : out boolean) do
HuboGanador := true;
if Nombre = Ganador then
JugadorSigueJugando := NO_SIGUES_JUGANDO;
Tablon(Nombre).SigueJugando := False;
else
if N /= 2 then
JugadorSigueJugando := SIGUES_JUGANDO;
Tablon(Nombre).SigueJugando := True;
else
JugadorSigueJugando := HAS_PERDIDO;
Tablon(Nombre).SigueJugando := False;
end if;
end if;
end SiguesJugando;
end loop;
end if;
NumeroPartida := NumeroPartida + 1;
if Ganador /= 0 then
N := N - 1;
end if;
end loop;
end Arbitro;
task body Jugador is
MiNombre : TNombre;
NumJug : NumMaxJugadores;
Apuesta : TApuesta;
ApuestaValidada : Boolean;
Mano : Tmano;
G : Integer_Random.Generator;
YoSigo : Estados;
Terminar : Boolean := False;
HuboGanador : boolean;
begin
Arbitro.AsignaNombre(MiNombre, NumJug);
-- Si MiNombre es -1, entonces termina su ejecución. Se sigue
-- este método para ceñirnos a los jugadores que quiere el
-- usuario
if MiNombre /= -1 then
-- Semillas aleatorias
Integer_Random.Reset(G);
while not Terminar loop
-- Envia Apuesta
for I in 1..MiNombre loop
Apuesta := Integer_Random.Random(G) mod (NumJug * 3);
end loop;
Arbitro.EnviaApuesta(MiNombre, Apuesta);
-- Proceso de confirmación de apuesta
ApuestaValidada := False ;
while not ApuestaValidada loop
Arbitro.ConfirmaApuesta(ApuestaValidada);
if not ApuestaValidada then
-- Genera Nueva apuesta
for I in 1..MiNombre loop
Apuesta := Integer_Random.Random(G) mod (NumJug * 3) ;
end loop;
Arbitro.ReEnviaApuesta(Apuesta);
end if;
end loop;
-- Envía Mano
for I in 1..MiNombre loop
Mano := Integer_Random.Random(G) mod 4;
end loop;
Arbitro.EnviaMano(MiNombre, Mano);
-- Comprueba su estado, si sigue jugando, si ha perdido o
-- si ha ganado y deja de jugar
Arbitro.SiguesJugando(MiNombre, YoSigo, HuboGanador);
if YoSigo = SIGUES_JUGANDO then
Terminar := False;
else
if YoSigo = NO_SIGUES_JUGANDO then
Terminar := True;
else
-- Ha perdido
Put_Line("Jugador " & MiNombre'Img &
": He perdido, tengo que pagar :_(");
end if;
end if;
if HuboGanador then
NumJug := NumJug - 1;
end if;
end loop;
end if;
end Jugador;
Jugadores : array (1..MAX) of Jugador;
NumJug : Natural;
begin
if Ada.Command_Line.Argument_Count /= 1 then
-- Número incorrecto de parámetros
Put_Line("Uso: ./chinos <num_jugadores>");
NumJug := 1;
else
NumJug := Integer'Value(Ada.Command_Line.Argument(1));
if NumJug < 2 then
-- Número mínimo de jugadores
Put_Line("El número de jugadores ha de ser mayor que 1." &
NumJug'Img & " no es mayor que 1");
Put_Line("Seleccione un valor mayor o igual que 2");
NumJug := 1;
end if;
if NumJug > MAX then
-- Número máximo de jugadores
Put_Line(NumJug'Img & " es mayor que " & MAX'Img);
Put_Line("Seleccione un valor menor o igual que " &
MAX'Img);
NumJug := 1;
end if;
end if;
Arbitro.FijaNumeroJugadores(NumJug);
-- Por si nos intentan colar algún valor no válido
exception
when Constraint_Error =>
NumJug := 1;
Arbitro.FijaNumeroJugadores(NumJug);
Put_Line("El Valor Introducido no es correcto.");
Put_Line("Uso: ./chinos <num_jugadores>");
end Chinos;
Manual de referencia de Ada
GLADE: programación distribuida
Introducción a GNAT-GLADE
En primer lugar hay que aclarar que el nombre de esta librería puede confundir a los usuarios y programadores de GTK+ y GNOME. Existe una aplicación muy extendida para el diseño de interfaces gráficas que se llama 'Glade'. Un gran número de lenguajes de programación disponen de librerías para poder leer los ficheros de interfaces que genera Glade (C, C++, Ada, Python, Scheme, Ruby, Eiffel, etc). Pues bien, GNAT-GLADE no tiene nada que ver con esta (magnífica ;-) herramienta.
GLADE (GNAT Library for Ada Distributed Environments) es una extensión para GNAT, el compilador libre (licenciado bajo GPL) de Ada 95, que permite desarrollar aplicaciones distribuidas basándose en el anexo del manual de referencia de Ada: Annex E: Distributed Systems.
La base de las aplicaciones distribuidas de Ada 95 son las particiones. Básicamente una aplicación distribuida se compone de al menos un par de particiones.
Es posible utilizar GNAT-GLADE de dos formas diferentes:
- Con varias particiones sobre la misma máquina.
- Con varias particiones sobre diferentes máquinas que formen parte de una red de computadoras.
Desde luego resulta mucho más interesante la segunda de las opciones. Es más, para desarrollar aplicaciones con varias particiones sobre una misma máquina hay muchos casos en que sería más conveniente no utilizar GLADE y basarse únicamente en los mecanismos de concurrencia de Ada (las tareas): la aplicación será más eficiente.
¿Cómo funciona GNAT-GLADE?
Cada una de las particiones de una aplicación basada en GNAT-GLADE, a la hora de la compilación se va a convertir en un ejecutable independiente. Cada uno de estos ejecutables serán los que se ejecuten por separado y se comuniquen entre ellos.
Existe una herramienta que facilita todo este proceso: gnatdist.
gnatdist lee un fichero de configuración en el que se especifica cómo queremos distribuir la aplicación y genera todos los ejecutables necesarios. De esta forma, es posible probar diferentes formas de distribuir una misma aplicación simplemente con lanzar gnatdist con un fichero de configuración distinto, sin necesidad de modificar el código de la aplicación.
Lenguaje de configuración de gnatdist
Las configuraciones de gnatdist se escriben en un lenguaje muy parecido a Ada. Es importante que todas las configuraciones se guarden en ficheros con extensión ".cfg".
Para lanzar la compilación de una aplicación distribuida con gnatdist únicamente es necesario ejecutar esta herramienta dándole como parámetro el nombre del fichero de configuración. Por ejemplo:
gnatdist Ejemplo_Configuracion1.cfg
¿Cómo se escriben las configuraciones?
En cualquier punto de la configuración es posible usar comentarios, que al igual que en Ada se comienzan con los caracteres --
.
Los ficheros de configuración han de contener un bloque "configuration", cuyo nombre ha de coincidir, al igual que en el caso de los paquetes, con el nombre del fichero en el que se encuentra. Es decir:
configuration Ejemplo_Configuracion1 is -- -- Código de la configuración -- end Ejemplo_Configuracion1;
Primer ejemplo
El movimiento se aprende andando; así que, vamos a por el primer ejemplo.
En este ejemplo vamos a crear una pequeña aplicación compuesta únicamente de dos particiones:
- La primera de ellas es un servidor de sumas y restas (al más puro estilo RPC). En él se van a definir dos operaciones: suma y resta, que dados dos números enteros, van a devolver el resultado de aplicar la operación elegida sobre ambos.
- La segunda de ellas es un pequeño cliente que efectuará un par de operaciones para comprobar que efectivamente el servidor responde.
calculadora.ads
package
Calculadorais
pragma
Remote_Call_Interface;function
Sumar (Operando1, Operando2 : Integer)return
Integer;function
Restar (Operando1, Operando2 : Integer)return
Integer;end
Calculadora;
En esta definición del paquete llama la atención la instrucción 'pragma'. Un pragma es simplemente una directiva para el compilador. En concreto, en este ejemplo, el
hace que se exporte la interfaz del paquete para que otras particiones puedan realizar llamadas a sus funciones, es decir, básicamente una llamada RPC.
pragma
Remote_Call_Interface
calculadora.adb
package
body
Calculadorais
function
Sumar (Operando1, Operando2 : Integer)return
Integeris
begin
return
Operando1 + Operando2;end
Sumar;function
Restar (Operando1, Operando2 : Integer)return
Integeris
begin
return
Operando1 - Operando2;end
Restar;end
Calculadora;
Este fichero es únicamente la implementación de las funciones del paquete calculadora.
cliente.adb
with
Ada.Text_IO;use
Ada.Text_IO;with
Calculadora;procedure
Clienteis
begin
Put_Line ("Calculadora, ¿cuanto es 321+123? "
& Integer'Image (Calculadora.Sumar (321,123))); Put_Line ("Calculadora, ¿cuanto es 321-123? "
& Integer'Image (Calculadora.Restar (321,123)));end
Cliente;
Por último, el cliente. Este programa hace un par de llamadas a las funciones exportadas por el proceso de calculadora. Como se puede ver, el código no tiene en cuenta si el proceso calculadora se encuentra corriendo en la misma máquina o en otra. Simplemente, realiza llamadas a las funciones de la calculadora. De todo lo demás, que es mucho, ya se ha encargado gnatdist y se encarga Ada.
ejemplo.cfg
configuration ejemplo is pragma Starter (Ada); Particion1 : Partition := (Calculadora); Particion2 : Partition := (Cliente); procedure Cliente is in Particion2; end ejemplo;
Este es el fichero de configuración/compilación de gnatdist.
El pragma Starter
describe como queremos que gnatdist compile el proyecto. Existen tres posibilidades: Ada
, Shell
y None
. En el primero de los casos será uno de los ejecutables el que lance todos los demás. Mediante Shell
, es un shell script el que lanzará los procesos. Con None
, tendremos que lanzarlos a mano o hacer nuestro propio script de arranque.
A continuación se definen las dos particiones que se han utilizado en este ejemplo: una para la calculadora y la segunda para el cliente que le realiza peticiones.
Por último se especifica cuál es la parte principal (el main). Esta partición, lógicamente, ha de tener un body.
Cuidado con los nombres de los ficheros: han de coincidir con el nombre del paquete y además, han de estar en minúsculas. De no ser así gnatdist producirá un error.
Compilación y ejecución del programa
Para compilar la aplicación, como ya hemos visto, simplemente hay que ejecutar gnatdist:
gnatdist ejemplo.cfg
Si no ha habido ningún problema, se habrá producido una salida como esta:
gnatdist: checking configuration consistency ------------------------------ ---- Configuration report ---- ------------------------------ Configuration : Name : ejemplo Main : cliente Starter : Ada code Partition particion1 Units : - calculadora (rci) Partition particion2 Main : cliente Units : Name : ejemplo Main : cliente Starter : Ada code Partition particion1 Units : - calculadora (rci) Partition particion2 Main : cliente Units : - cliente (normal) ------------------------------- gnatdist: building calculadora caller stubs from calculadora.ads gnatdist: building calculadora receiver stubs from calculadora.adb gnatdist: building partition particion1 gnatdist: building partition particion2 gnatdist: generating starter cliente
Nota: las siguientes copias de salidas de comandos son de un sistema GNU/Linux, pero en otros sistemas operativos existirá un equivalente.
En este momento ya tenemos construidos todos los ejecutables:
lrwxrwxrwx 1 alo alo 10 Oct 9 22:33 cliente -> particion2 -rwxr-xr-x 1 alo alo 3663802 Oct 9 22:33 particion1 -rwxr-xr-x 1 alo alo 3723193 Oct 9 22:33 particion2
Como podemos ver, al especificar el Pragma Starter (Ada)
en el fichero de configuración, gnatdist ha generado un enlace simbólico al ejecutable que va a lanzar los demás.
En esta prueba vamos a ejecutar los dos programas en la misma máquina, más adelante se explicará cómo hacerlo en varias.
$ ./cliente Host for "particion1": localhost - Calculadora, ¿cuanto es 321+123? = 444 - Calculadora, ¿cuanto es 321-123? = 198
Ahora bien, ¿estamos seguros de que se ha ejecutado un programa paralelo?, ¿estamos seguros de que en realidad no se ha enlazado el paquete Calculadora en los dos ejecutables?
Como podemos ver en los procesos, realmente se han ejecutado los dos programas:
[..] 7701 pts/10 S 0:00 \_ bash 8753 pts/10 S 0:00 | \_ ./cliente 8754 pts/10 S 0:00 | \_ ./cliente 8755 pts/10 S 0:00 | \_ ./cliente 8793 pts/10 S 0:00 | \_ ./cliente [..] 8788 ? S 0:00 /home/alo/prog/glade/1/particion1 --detach --boot_location tcp://localhost:35802 8790 ? R 0:00 \_ /home/alo/prog/glade/1/particion1 --detach --boot_location tcp://localhost:35802 8791 ? S 0:00 \_ /home/alo/prog/glade/1/particion1 --detach --boot_location tcp://localhost:35802 8792 ? S 0:00 \_ /home/alo/prog/glade/1/particion1 --detach --boot_location tcp://localhost:35802 8794 ? R 0:00 \_ /home/alo/prog/glade/1/particion1 --detach --boot_location tcp://localhost:35802
Es más, si examinamos los símbolos de los dos ejecutables, podemos ver como la calculadora tiene enlazadas las funciones de suma y resta:
08050500 g F .text 00000011 calculadora__restar 080504f0 g F .text 0000000d calculadora__sumar
y el cliente, además, tiene las funciones que usa gnatdist para el acceso:
080502c0 l F .text 00000018 calculadora__sumar___clean.0 080502e0 l F .text 0000005a calculadora__sumar___input27___read30.2 08050340 l F .text 0000018a calculadora__sumar___input27.1 080504d0 g F .text 000002bf calculadora__sumar 08050790 l F .text 00000018 calculadora__restar___clean.3 080507b0 l F .text 0000005a calculadora__restar___input67___read70.5 08050810 l F .text 0000018a calculadora__restar___input67.4 080509a0 g F .text 000002bf calculadora__restar
Instalación bajo Debian GNU/Linux
Instalación de GNAT (compilador de Ada 95) y GLADE para GNAT (extensión para soporte de programación distribuida).
Para realizar la instalación en el sistema es imprescindible estar identificado como usuario root:
# apt-get install gnat gnat-glade
Instalación de la documentación de ambos paquetes. Estos paquetes son opcionales aunque muy recomendables.
# apt-get install gnat-glade-doc gnat-doc
Enlaces externos
- Ada: Anexo de sistemas distribuidos. Alejandro Alonso. DIT/UPM.
- RT-GLADE: Implementación Optimizada para Tiempo Real del Anexo de Sistemas Distribuidos de Ada 95. Universidad de Cantabria
- Página oficial de la versión pública de GLADE
- Página de AdaCore sobre la versión soportada de GLADE
- GLADE User Guide
Manual de referencia de Ada
Autores
- Artículo original de Alvaro López Ortega que se puede encontrar en Programación distribuida con Ada 95 bajo GNU/Linux (I). El autor ha aceptado publicarlo bajo licencia GFDL, ver la autorización.
- Copyright © 2001 por Alvaro López Ortega
- Copyright © 2005 por los autores de esta versión. Consulte el historial.
Unidades predefinidas
El estándar de Ada incluye una serie de unidades predefinidas útiles para muchas tareas comunes de programación y que ha de proporcionar todo compilador de Ada.
Algunas de estas unidades son:
- Paquete Standard
- Paquete System
- Paquete Ada
- Paquete Ada.Command_Line
- Paquete Ada.Exceptions
- Paquete Ada.Numerics
- Paquete Ada.Numerics.Complex_Elementary_Functions - G.1.2
- Paquete Ada.Numerics.Complex_Types - G.1.1
- Paquete Ada.Numerics.Discrete_Random - A.5.2
- Paquete Ada.Numerics.Elementary_Functions - A.5.1
- Paquete Ada.Numerics.Float_Random - A.5.2
- Paquete Ada.Numerics.Generic_Complex_Elementary_Functions - G.1.2
- Paquete Ada.Numerics.Generic_Complex_Types - G.1.1
- Paquete Ada.Numerics.Generic_Elementary_Functions - A.5.1
- Paquete Ada.Real_Time
- Paquete Ada.Strings
- Paquete Ada.Text_IO
- Paquete Ada.Float_Text_IO
- Paquete Ada.Integer_Text_IO
- Paquete Ada.Sequential_IO
- Función genérica Ada.Unchecked_Conversion
- Procedimiento genérico Ada.Unchecked_Deallocation
- Paquete Interfaces
Manual de refencia de Ada
Unidades predefinidas. Standard
Paquete Standard
La unidad Standard es un paquete especial que contiene las declaraciones de todos los tipos predefinidos tales como Integer o Boolean.
En términos de compilación es como si todas las unidades tuviesen escrito antes de nada:
with
Standard;use
Standard;
También posee un
paquete anidado llamado ASCII, que contiene las constantes que definen
los caracteres tales como CR (retorno de carro) o LF (avance de línea).
Escribiendo use
ASCII; se pueden referenciar simplemente como CR. Sin embargo el estándar de Ada 95 no nos recomienda usar el paquete ASCII (caracteres de 7 bits), porque éste queda obsoleto tras la definición del paquete Ada.Characters.Latin_1, que define el juego completo de caracteres ISO Latín 1 de 8 bits.
Especificación
La definición del paquete Standard según el manual de referencia de Ada:
package Standard is
pragma Pure(Standard);
type Boolean is (False, True);
-- The predefined relational operators for this type are as follows:
-- function "=" (Left, Right : Boolean'Base) return Boolean;
-- function "/=" (Left, Right : Boolean'Base) return Boolean;
-- function "<" (Left, Right : Boolean'Base) return Boolean;
-- function "<=" (Left, Right : Boolean'Base) return Boolean;
-- function ">" (Left, Right : Boolean'Base) return Boolean;
-- function ">=" (Left, Right : Boolean'Base) return Boolean;
-- The predefined logical operators and the predefined logical
-- negation operator are as follows:
-- function "and" (Left, Right : Boolean'Base) return Boolean;
-- function "or" (Left, Right : Boolean'Base) return Boolean;
-- function "xor" (Left, Right : Boolean'Base) return Boolean;
-- function "not" (Right : Boolean'Base) return Boolean;
-- The integer type root_integer is predefined.
-- The corresponding universal type is universal_integer.
type Integer is range ''implementation-defined'';
subtype Natural is Integer range 0 .. Integer'Last;
subtype Positive is Integer range 1 .. Integer'Last;
-- The predefined operators for type Integer are as follows:
-- function "=" (Left, Right : Integer'Base) return Boolean;
-- function "/=" (Left, Right : Integer'Base) return Boolean;
-- function "<" (Left, Right : Integer'Base) return Boolean;
-- function "<=" (Left, Right : Integer'Base) return Boolean;
-- function ">" (Left, Right : Integer'Base) return Boolean;
-- function ">=" (Left, Right : Integer'Base) return Boolean;
-- function "+" (Right : Integer'Base) return Integer'Base;
-- function "-" (Right : Integer'Base) return Integer'Base;
-- function "abs" (Right : Integer'Base) return Integer'Base;
-- function "+" (Left, Right : Integer'Base) return Integer'Base;
-- function "-" (Left, Right : Integer'Base) return Integer'Base;
-- function "*" (Left, Right : Integer'Base) return Integer'Base;
-- function "/" (Left, Right : Integer'Base) return Integer'Base;
-- function "rem" (Left, Right : Integer'Base) return Integer'Base;
-- function "mod" (Left, Right : Integer'Base) return Integer'Base;
-- function "**" (Left : Integer'Base; Right : Natural)
-- return Integer'Base;
-- The specification of each operator for the type
-- root_integer, or for any additional predefined integer
-- type, is obtained by replacing Integer by the name of the type
-- in the specification of the corresponding operator of the type
-- Integer. The right operand of the exponentiation operator
-- remains as subtype Natural.
-- The floating point type root_real is predefined.
-- The corresponding universal type is universal_real.
type Float is digits ''implementation-defined'';
-- The predefined operators for this type are as follows:
-- function "=" (Left, Right : Float) return Boolean;
-- function "/=" (Left, Right : Float) return Boolean;
-- function "<" (Left, Right : Float) return Boolean;
-- function "<=" (Left, Right : Float) return Boolean;
-- function ">" (Left, Right : Float) return Boolean;
-- function ">=" (Left, Right : Float) return Boolean;
-- function "+" (Right : Float) return Float;
-- function "-" (Right : Float) return Float;
-- function "abs" (Right : Float) return Float;
-- function "+" (Left, Right : Float) return Float;
-- function "-" (Left, Right : Float) return Float;
-- function "*" (Left, Right : Float) return Float;
-- function "/" (Left, Right : Float) return Float;
-- function "**" (Left : Float; Right : Integer'Base) return Float;
-- The specification of each operator for the type root_real, or for
-- any additional predefined floating point type, is obtained by
-- replacing Float by the name of the type in the specification of the
-- corresponding operator of the type Float.
-- In addition, the following operators are predefined for the root
-- numeric types:
function "*" (Left : root_integer; Right : root_real)
return root_real;
function "*" (Left : root_real; Right : root_integer)
return root_real;
function "/" (Left : root_real; Right : root_integer)
return root_real;
-- The type universal_fixed is predefined.
-- The only multiplying operators defined between
-- fixed point types are
function "*" (Left : universal_fixed; Right : universal_fixed)
return universal_fixed;
function "/" (Left : universal_fixed; Right : universal_fixed)
return universal_fixed;
-- The declaration of type Character is based on the standard ISO 8859-1 character set.
-- There are no character literals corresponding to the positions for control characters.
-- They are indicated in italics in this definition. See 3.5.2.
type Character is
(nul, soh, stx, etx, eot, enq, ack, bel, --0 (16#00#) .. 7 (16#07#)
bs, ht, lf, vt, ff, cr, so, si, --8 (16#08#) .. 15 (16#0F#)
dle, dc1, dc2, dc3, dc4, nak, syn, etb, --16 (16#10#) .. 23 (16#17#)
can, em, sub, esc, fs, gs, rs, us, --24 (16#18#) .. 31 (16#1F#)
' ', '!', '"', '#', '$', '%', '&', ''', --32 (16#20#) .. 39 (16#27#) "
'(', ')', '*', '+', ',', '-', '.', '/', --40 (16#28#) .. 47 (16#2F#)
'0', '1', '2', '3', '4', '5', '6', '7', --48 (16#30#) .. 55 (16#37#)
'8', '9', ':', ';', '<', '=', '>', '?', --56 (16#38#) .. 63 (16#3F#)
'@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', --64 (16#40#) .. 71 (16#47#)
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', --72 (16#48#) .. 79 (16#4F#)
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', --80 (16#50#) .. 87 (16#57#)
'X', 'Y', 'Z', '[', '\', ']', '^', '_', --88 (16#58#) .. 95 (16#5F#)
'`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', --96 (16#60#) .. 103 (16#67#)
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', --104 (16#68#) .. 111 (16#6F#)
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', --112 (16#70#) .. 119 (16#77#)
'x', 'y', 'z', '{', '|', '}', '~', del, --120 (16#78#) .. 127 (16#7F#)
reserved_128, reserved_129, bph, nbh, --128 (16#80#) .. 131 (16#83#)
reserved_132, nel, ssa, esa, --132 (16#84#) .. 135 (16#87#)
hts, htj, vts, pld, plu, ri, ss2, ss3, --136 (16#88#) .. 143 (16#8F#)
dcs, pu1, pu2, sts, cch, mw, spa, epa, --144 (16#90#) .. 151 (16#97#)
sos, reserved_153, sci, csi, --152 (16#98#) .. 155 (16#9B#)
st, osc, pm, apc, --156 (16#9C#) .. 159 (16#9F#)
' ', '¡', '¢', '£', '¤', '¥', '¦', '§', --160 (16#A0#) .. 167 (16#A7#)
'¨', '©', 'ª', '«', '¬', '', '®', '¯', --168 (16#A8#) .. 175 (16#AF#)
'°', '±', '²', '³', '´', 'µ', '¶', '·', --176 (16#B0#) .. 183 (16#B7#)
'¸', '¹', 'º', '»', '¼', '½', '¾', '¿', --184 (16#B8#) .. 191 (16#BF#)
'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', --192 (16#C0#) .. 199 (16#C7#)
'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', --200 (16#C8#) .. 207 (16#CF#)
'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', --208 (16#D0#) .. 215 (16#D7#)
'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', --216 (16#D8#) .. 223 (16#DF#)
'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', --224 (16#E0#) .. 231 (16#E7#)
'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', --232 (16#E8#) .. 239 (16#EF#)
'ð', 'ñ', 'ò', 'ó', 'ô',