EL LENGUAJE DE PROGRAMACION “ADA”

DEPTO. DE SISTEMAS DE INFORMACION FACULTAD DE CIENCIAS EMPRESARIALES UNIVERSIDAD DEL BIO-BIOLENGUAJE DE PROGRAMACION ADA

Extractado por Eduardo Jara de J.G.P. Barnes. Programmining in Ada.

Contenido

1. Introducción …………………………………………………………………….. 01

2. Descripción general ………………………………………………………….. 03

3. Estilo léxico …………………………………………………………………….. 07

4. Tipos escalares ………………………………………………………………… 10

5. Estructuras de control ……………………………………………………….. 23

6. Tipos compuestos …………………………………………………………….. 33

7. Subprogramas ………………………………………………………………….. 46

8. Estructura general …………………………………………………………….. 60

9.  Tipos Privados  ………………………………………………………………. 66

10. Excepciones …………………………………………………………………. 70

11. Genéricos …………………………………………………….. 78

12. Tareas …………………………………………………………. 63

1.  Introducción

La historia de Ada comienza en 1974 cuando el Departamento de Defensa de los Estados Unidos (DoD) se percató que estaba gastando demasiado en software. Se llevó a cabo un estudio detallado sobre la distribución  de  los  costos  y  se  descubrió  que  sobre  la  mitad  de  éstos  estaba  directamente  relacionado  con sistemas incrustados (embedded)

Se realizó un análisis de los lenguajes utilizados en diferentes áreas. Se descubrió que COBOL era el estandar para el procesamiento de datos y FORTRAN lo era para cálculos científicos y numéricos. Aunque estos  lenguajes  no  eran  modernos,  el  hecho  que  fueran  uniformemente  utilizados  en  sus  respectivas  áreas evitaba duplicaciones.

La  situación  con  respecto  a  los  sistemas  incrustados  era  diferente.  La  cantidad  de  lenguajes utilizados era enorme.   No sólo cada unidad militar tenía su lenguaje de alto nivel favorito, sino que usaban varios  lenguajes  assembler.  En  resultado  era  que  había  gastos  innecesarios  en  compiladores  y  costos adicionales en entrenamiento y mantención debido a la falta de estandarización.

Se determinó que la única forma de controlar efectivamente los costos en los sistemas incrustados

era estandarizar el uso de lenguajes de programación. El primer paso en  esta dirección fue  la generación de

un documento en que se delineaban los requerimientos del lenguaje estandar. La primera versión (Strawman)

fue  publicada  en  1975.  Después  de  recibir  comentarios  de  diversas  fuentes  el  documento  fue  refinado (Woodenman).  En  junio  de  1976  se  produjo  una  nueva  versión  (Tinman).  Este  era  un  documento  más específico e identificaba la funcionalidad que se requería del lenguaje.

En esta etapa se evaluaron varios lenguajes existentes respecto a la especificación Tinman. Como se podría esperar ninguno de éstos satisfacía totalmente los requerimientos; por otro lado la impresión general

era  que  sería  necesario  crear  un  nuevo  lenguaje  basado  en  conceptos  de  vanguardia  en  el  área  de  la programación.

Los lenguajes existentes fueron clasificados en tres categorías:

a)   ”no apropiados”:  Lenguajes obsoletos u orientados a otras áreas que no fueron considerados en las etapas siguientes. Por ejemplo, FORTRAN y  CORAL 66.

b)  ”no    inapropiados”:    Estos    lenguajes    tampoco    eran    satisfactorios,    pero    tenían    algunas características  interesantes  que  podían  ser  tomadas  como  ”inspiración”  para  enriquecer  el estandar. Por ejemplo,  RTL/2 y LIS.

c)   ”bases recomendadas”: Los lenguajes Pascal, PL/I y Algol 68 fueron considerados como posibles puntos de partida para el diseño del lenguaje final.

En este punto el documento de requerimientos fue revisado y reorganizado (Ironman). Se llamó a propuestas para el diseño del lenguaje. Se recibieron diecisiete propuestas de las cuales se eligieron tres para que “compitieran” en paralelo. Los cuatro elegidos fueron CII Honeywell Bull (verde), Intermetrics (Rojo), Softech (Azul) y SRI International (Amarillo). Los códigos de color se introdujeron para que la comparación

se realizara anónimamente.

Los  diseños  iniciales  aparecieron  a  comienzos  de  1978  y    fueron  analizados  por  varios  grupos alrededor  del  mundo.  El  DoD  juzgó  que  los  diseños  Verde  y  Rojo  eran  más  promisorios  que  los  Azul  y Amarillo y éstos últimos fueron eliminados.

Entonces,  el  desarrollo  entró  en  una  segunda  fase  y  se  dio  a  los  desarrolladores  un  año  más  para refinar sus diseños. Los requerimientos también fueron mejorados a la luz de la retroalimentación recibida de

los diseños iniciales (Steelman).

La  elección  final  del  lenguaje  fue  hecha  el  2  de  mayo  de  1979  cuando  el  ”lenguaje  verde” desarrollado en   CII Honeywell Bull   por un equipo internacional liderado por Jean Ichbiah   fue declarado ganador.

Entonces el DoD anunció que el nuevo lenguaje sería conocido como Ada en honor de Augusta Ada Byron, condesa de Lovelace (1815-1852). Ada, hija de Lord Byron, fue la asistente y mecenas de Charles Babbage  y  trabajó  en  su  ”máquina  analítica”.  En  un  cierto  sentido  ella  fue  la  primera  programadora  de  la historia.

Entonces  el  desarrollo  de  Ada  entró  a  una  tercera  etapa,  el  propósito  de  la  cual  fue  el  que  los eventuales  usuarios  hicieran  sus  comentarios  respecto  a  que  tan  conveniente  era  el  lenguaje  para  sus necesidades.  Se continuó con otros estudios de los  que se concluyó que Ada era un buen lenguaje, pero que

en algunas áreas todavía se requerían algunos refinamientos.    Después de esto (en julio de 1980) se publicó

la primera versión definitiva del lenguaje y se la propuso a la ANSI (America National Standards Institute)

como un estandar.

La estandarización por parte de ANSI tomó unos dos años y se le introdujeron algunos cambios a

Ada. El Manual de Referencia del Lenguaje estandar de la ANSI fue finalmente publicado en enero de 1983.

A esta versión del lenguaje se le conoce como Ada 83.

Tan pronto como se comenzó a utilizar el lenguaje se iniciaron los estudios para su mejora basada en

la experiencia práctica de los usuarios con el lenguaje. A la nueva versión  se le denominó Ada 9X, entre las principales  mejoras  hechas  sobre  la   anterior  se  cuenta  la  incorporación  de  mecanismo  de  herencia  en  el manejo  de  tipos  con  que  contaba  Ada  83,  el  cual,  a  pesar  de  ser  muy  poderoso,  al  carecer  de  herencia herencia no se adecuaba al paradigma de Orientación a Objetos. A esta segunda versión de Ada se denomina actualmente como Ada 95.

Ada  es  un  lenguaje  grande  en  la  medida  que  enfrenta  la  mayoría  de  los  aspectos  relevantes  a  la programación de sistemas prácticos en el mundo real. Por ejemplo, es mucho más grande que Pascal, el que a pesar  de  sus  extensiones  realmente  sólo  es  adecuado  para  propósitos  de  entrenamiento  (para  lo  que  fue diseñado) y para programas pequeños. A continuación se enumeran las principales características de Ada:

a)   Legibilidad: se reconoce que los programas profesionales se leen con mayor frecuencia de lo que son  escritos.  Por  lo  tanto  es  importante  evitar  una  notación  lacónica  como  en  C,   que  permite escribir un programa rápidamente, pero que hace casi imposible entenderlo, excepto para el autor

al poco tiempo de haberlo escrito.

b)  Tipificación    fuerte:  esto  asegura  que  cada  objeto  tiene   un  conjunto  claramente  definido  de posibles   valores   y   previene   la   confusión   entre   conceptos   lógicamente   distintos.                                             Como consecuencia de esto, muchos errores pueden ser detectados en tiempo de compilación, en otros lenguajes esto podría conducir a programas ejecutables, pero incorrectos.

c)   Programación en gran escala:  se necesitan mecanismos de encapsulación, compilación separada

y manejo de bibliotecas para escribir  programas portables y  mantenibles.

d)  Manejo de excepciones: es un hecho que los programas raramente son correctos en un cien por ciento.  Por este motivo se hace necesario proveer un medio por el cual los programas puedan ser construidos de forma tal que los errores en una parte de éste no repercutan  en las demás.

e)   Abstracción  de  datos:  como  ya  se  ha  mencionado,  se  puede  lograr  mayor  portabilidad  y mantenibilidad  si  los  detalles  de  la  representación  de  los  datos  puede  ser  separada                                               de  la especificación de las operaciones lógicas sobre los datos.

f)   Tareas:  en  muchos  casos  es  importante  que  el  programa  sea  concebido  como  una  serie  de actividades paralelas en lugar de una secuencia simple de acciones. Al entregar estas facilidades dentro  del  lenguaje  y  no  a  través  de  llamadas  a  un  sistema  operativo  se  logra  una  mayor portabilidad y mantenibilidad.

g)  Unidades  genéricas:  es  muchos  casos  la  parte  lógica  de  un  programa  es  independiente  de  los tipos de valores que son manipulados. Por lo tanto se requiere de un mecanismo para la creación

de partes lógicamente relacionadas a partir de un prototipo único. Esto es especialmente útil para

la creación de bibliotecas.

2. Descripción general

Uno  de  los  aspectos  más  importantes  en  la  programación  es  el  reuso  de  partes  de  programas existentes  para  que  el  esfuerzo  de  generar  nuevo  código  sea  mínimo.  Así  el  concepto  de  biblioteca  de programas emerge en forma natural y un aspecto importante de un lenguaje de programación es su habilidad para expresar el cómo reusar  itemes de una biblioteca.

Ada reconoce esta necesidad e introduce el concepto de unidades de biblioteca (library units). Un programa Ada completo es concebido como un programa principal (en sí mismo una unidad de biblioteca) que solicita los servicios de otras unidades.

Supongamos que queremos escribir un programa que imprime la raíz cuadrada de un cierto número, por ejemplo, 2.5. Podría esperarse  que estén disponibles bibliotecas que nos provean servicios para calcular raíces cuadradas y entregar datos. De este modo, nuestro trabajo se reduciría a escribir un programa principal que haga uso de estos servicios en el modo que deseemos.

Supondremos que en nuestra biblioteca existe una función de nombre SQRT que permite calcular raíces cuadradas, y   que, además,   nuestra biblioteca cuenta con un paquete (package) llamado SIMPLE_IO que  contiene  servicios  simples  de entrada-salida (lectura y escritura de números,   escrituras de cadenas de caracteres, etc.). Nuestro programa sería:

with SQRT, SIMPLE_IO; procedure PRINT_ROOT is use SIMPE_IO;

begin PUT(SQRT(2.5)); end PRINT_ROOT;

El programa está escrito como un procedimiento llamado PRINT_ROOT  precedido por una cláusula with  donde se dan los nombre de las unidades de biblioteca que se desea usar. El cuerpo del procedimiento contiene una única instrucción:

PUT(SQRT(2.5)); Al escribir

use SIMPLE_IO;

se obtiene acceso inmediato a los servicios del paquete SIMPLE_IO. Si se hubiese omitido la cláusula use

hubiese sido necesario usar la “notación punto”.

SIMPLE_IO.PUT(SQRT(2.5));

para indicar dónde debe buscarse el procedimiento PUT.

Podemos hacer nuestro programa más útil haciendo que lea el número cuya raíz cuadrada se desea obtener.  El programa quedaría:

with SQRT, SIMPLE_IO; procedure PRINT_ROOT is use SIMPE_IO;

X : FLOAT;

begin

GET(X); PUT(SQRT(X)); end PRINT_ROOT;

La estructura general del procedimiento es clara: entre is y begin podemos escribir declaraciones, y entre  begin  y  end  escribimos  instrucciones  (operaciones).  En  términos  generales  podemos  decir  que  las declaraciones  presentan  las  entidades  que  queremos  manipular  y  las  instrucciones  indican  la  secuencia  de acciones a realizar.

Notemos algunos detalles:

  • Todas las declaraciones e instrucciones terminan con un punto y coma a diferencia de otros lenguajes como Algol y Pascal donde los punto y coma son utilizados como separadores en lugar de terminadores.
  • El programa contiene varios identificadores tales como procedure , PUT y X. Los identificadores se dividen en dos grandes grupos. Unos pocos (63 en Ada 83) como procedure e is , que son utilizados para

indicar la estructura del programa; son palabras reservadas y no pueden ser usados para otros propósitos. Los

otros tales como  PUT y X pueden ser usados para cualquier propósito  que se desee. Algunos de ellos (en nuestro ejemplo FLOAT) tienen un significado predefinido, pero podrían ser usados para otros propósitos, aunque se corre el riesgo de confusión en el código.

Finalmente  observemos  que  el  nombre  del  procedimiento,  PRINT_ROOT,  se  repite  entre  el  end final y el punto y coma. Esto es opcional, pero se recomienda para aclarar la estructura general, el cual es obvio en un ejemplo tan pequeño.

Nuestro  ejemplo  es demasiado  simple, sería más útil si se lee una serie de números y se imprime cada respuesta en una línea separada. Arbitrariamente definiremos que el  proceso terminará al ingresarse un valor igual a cero.

with SQRT, SIMPLE_IO;

procedure PRINT_ROOTS is use SIMPLE_IO;

X : FLOAT;

begin

PUT(”Roots of various numbers”); NEW_LINE(2);

loop

GET(X);

exit when X = 0.0; PUT(”Root of”); PUT(X); PUT(”is”);

if X < 0.0 then

PUT(”not calculable”);

else

end if;

PUT(SQRT(X));

NEW_LINE; end loop; NEW_LINE;

PUT(”Program finished”);

NEW_LINE;

end PRINT_ROOTS;

La salida del programa ha sido mejorada mediante la llamada a los procedimientos NEW_LINE y PUT  incluidos  en   el  paquete  SIMPLE_IO.  El  parámetro  numérico  entero  del  procedimiento  NEW_LINE indica  cuantas  líneas  se  deberá  escribir,  si  no  se  entrega  el  parámetro  se  asume  un  valor  igual  a  1  (uno). También  hay  una  llamada  a  PUT  con  una  cadena  como  argumento.  Este  es  de  hecho  un  procedimiento diferente al que escribe el número X. El compilador diferencia cual corresponde en cada caso de acuerdo al tipo (y/o cantidad) de parámetros utilizado.

En Ada se debe observar reglas estrictas para cerrar estructuras; loop es cerrado con end loop e if con enf if. Todas las estructuras de control de Ada tienen sus cierres en lugar de la forma abierta de Pascal que puede conducir a errores.

Veamos ahora la posible estructura de la función SQRT y el paquete SIMPLE_IO que en nuestro

ejemplo se han supuesto como existentes.

La función SQRT tendrá una estructura similar a nuestro programa, la mayor diferencia radica en la existencia de parámetros.

function SQRT (F:FLOAT) return FLOAT is

R: FLOAT;

begin

— calcular la raíz cuadrada de F y guardarla en R

return R;

end SQRT;

El paquete SIMPLE_IO consta de dos partes, la especificación que describe su interfaz con el mundo externo  (es  decir,  las  otras  unidades  de  biblioteca  que  hacen  uso  de  el  paquete)  y  el  cuerpo  (body)  que contiene los detalles de cómo han sido implementados los componentes del paquete. Si el paquete contiene sólo los procedimientos que hemos usado en nuestro ejemplo, su especificación sería:

package SIMPLE_IO is

procedure GET(F: out FLOAT); procedure PUT(F: in FLOAT); procedure  PUT(S: in STRING);

procedure NEW_LINE(N: in INTEGER:=1);

end SIMPLE_IO;

El  parámetro     de  GET  es  un  parámetro  out  (salida)  puesto  que  el  efecto  de  llamar  a  este procedimiento en   GET(X) es entregar un valor “desde dentro” del procedimiento por medio del parámetro

real  X  (actual  parameter  X).  Los  demás  parámetros  son  in  puesto  que  sirven  para  ingresar  datos  a  los procedimientos.

Sólo  una  parte  de  los  procedimientos  está  presente  en  la  especificación  del  paquete;  esta  parte  se conoce  como  la  ”especificación  de  los  procedimientos”  donde  sólo  se  entrega  la  información  que  permite llamar a los procedimientos. Es decir, para que nuestro programa PRINT_ROOTS use el procedimiento GET sólo  necesita  saber  el  nombre  de  éste  y  sus  parámetros,  los  detalles  de  implementación  en  esta  etapa  no importan. Esto significa que para compilar el programa PRINT_ROOTS sólo es necesario que esté presente

la  declaración  del  paquete  SIMPLE_IO.  Lógicamente,  para  poder  ejecutar  el  programa  se  necesitará  la implementación de todos los procedimientos.

En nuestro ejemplo podemos ver dos especificaciones superpuestas de PUT, una con un parámetro

de  tipo  FLOAT  y  otro  con  un  parámetro  de  tipo  STRING.  Es  importante  recalcar  que  estos  son  dos procedimientos diferentes, el compilador Ada discriminará de acuerdo a los parámetros de cada uno de ellos. Finalmente, notemos  como se asigna un valor por omisión al parámetro del procedimiento NEW_LINE.

El  cuerpo  del  paquete  (package  body)  SIMPLE_IO  contendrá  la  totalidad  de  los  cuerpos  de  los procedimientos especificados, además de otros elementos (variables, estructuras de datos, otras funciones o procedimientos) que se requieran para su implementación y que quedan de una forma natural escondidas de

los usuarios externos. Esto quiere decir que los usuarios del paquete (otras unidades de biblioteca: funciones, procedimientos, paquetes, tareas)  sólo “conocen” lo indicado en la especificación del paquete.  En términos generales el cuerpo del paquete tendría la siguiente estructura:

with INPUT_OUTPUT;

package body SIMPLE_IO is

. . .

procedure GET (F: out FLOAT) is

. . .

begin

. . .

end GET;

– otros procedimientos parecidos

end SIMPLE_IO

La  cláusula  with  muestra  que  la  implementación  (pero,  en  este  caso,  no  la  especificación)  de  los procedimientos  en  SIMPLE_IO  requiere  de  un  hipotético  paquete más general llamado INPUT_OUTPUT. Notemos, además, que en el cuerpo de GET se repite la especificación del procedimiento que fue entregada

en la especificación del paquete.

Es importante mencionar el paquete especial STANDARD. Este es un paquete que existe en toda implementación del lenguaje y contiene las declaraciones de todos los identificadores predefinidos tales como FLOAT y NUMERIC_ERROR. Se asume el acceso automático a este paquete y por lo tanto no es necesario

dar su nombre en una cláusula with.

En  resumen,  un  programa  Ada  es  concebido  como  un  conjunto  de  componentes  (procedimientos, funciones, paquetes, tareas) que se proveen servicios mutuamente.

Ejercicio: Seguramente la función SQRT no estará directamente disponible en una biblioteca, sino que será parte de un paquete junto con otras funciones matemáticas. Supongamos que dicho paquete tiene por nombre  SIMPLE_MATHS  y  los  otras  funciones  son  LOG,  EXP,  SIN  y  COS.  Se  pide  que  escriba  la especificación de dicho paquete (utilice la especificación de SIMPLE_IO como modelo) ¿Qué cambios habría que hacerle al programa PRINT_ROOTS?

3. Estilo léxico

Ciertamente no es muy agradable comenzar el estudio de un lenguaje con un tema tan árido como son  los  detalles  de  la  construcción  de  cosas  tales  como  identificadores  y  números,  sin  embargo  ello  es esencial  para  un  conocimiento  acabado  de  un  lenguaje  y,  obviamente,  para  la  correcta  construcción  de programas.

3.1. Elementos léxicos

Un  programa  Ada  se  escribe  como  una  secuencia  de  líneas  de  texto  que  contienen  los  siguientes caracteres:

  • alfabeto a-z y A-Z
  • dígitos 0-9
  • otros caracteres ” # & ´( ) * + , - / : ; < = > _ |
  • el carácter blanco

Se debe tener presente que los siguientes delimitadores compuestos no deben contener espacios:

  • => usado en when, cases, etc.
    .. usado para rangos
      ** para exponenciación
        := para asignación
          /= no igual
            >= mayor o igual
              <= menor o igual
                <> para arreglos

                3. 2. Identificadores

                Un identificador se define de la siguiente manera:

                identifier ::= letter {[underline] letter_or_digit}

                letter_or_digit ::= letter | digit

                letter ::= upper_case_letter | lower_case_letter

                Esto  quiere  decir  que  un  identificador  consiste  en  una  letra  seguida  (posiblemente)  de  una  o  más letras  o  dígitos  con  subrayados  aislados.  Se  pueden  usar  letras  mayúsculas  y  minúsculas,  las  que  no  son tomadas como diferenciadores de identificadores, por ejemplo:

                Sueldo_Base y SUELDO_BASE

                son el mismo identificador. A pesar de esta libertad para escribir los identificadores, se recomienda, para una mejor legibilidad  usar minúsculas para las palabras reservadas y mayúsculas para los demás identificadores.

                Ada  no  impone  un  límite  en  el  largo  del  identificador,  sin  embargo,  puede  haber  limitaciones impuestas  por  implementaciones  específicas.  De  esta  manara  se  estimula  el  uso  de  nombres  de  variables autodocumentados como SUELDO_BASE en lugar del poco significativo S.

                Ejercicio: Indique cuáles de los siguientes identificadores son incorrectos y por qué

                a)   Ada

                b)  fish&chips

                c)   RATE-OF-FLOW

                d)  UMO164G

                e)   TIME_ _LAG

                f)   77E2

                g)  X_

                h)  tax rate i)   goto

                3.3. Números

                Los números pueden ser enteros o reales. La mayor diferencia es que los reales siempre contienen un punto decimal y los enteros no. Es ilegal usar un entero donde el contexto indica que debe usarse un real y viceversa. Entonces

                AGE: INTEGER := 43.9; y               WEIGHT: REAL := 150;

                son expresiones incorrectas.

                La forma más simple de un entero es una secuencia de dígitos, y la de un real es una secuencia de dígitos con un punto decimal. Nótese que debe haber al menos un dígito a cada lado del punto decimal.

                A diferencia de otros lenguajes en Ada tanto los enteros como los reales pueden tener exponente, el cual se indica con una letra E (puede ser minúscula) seguida de un entero con o sin signo. El exponente no puede ser negativo en el caso de un entero (si así fuera el resultado podría no ser un entero). Por ejemplo el real 98.4 podría ser escrito con exponente de las siguientes formas:

                9.84E1        98.4e0         984.0e-1         0.984E+2

                pero 984e-1 sería incorrecto.

                En forma análoga, el entero 1900 podría escribirse con exponente como:

                19E2            190e+1          1900E+0

                pero  19000 e-1 sería incorrecto.

                Ejercicio: Indique cuáles de las siguientes secuencias de dígitos son reales o enteros válidos.

                a)   38.6

                b)  .5

                c)   32e2 d)  32e-2 e)   E+6

                f)   27.4e_2

                3.4. Comentarios

                ejemplo:

                Un  comentario  en  Ada  es  cualquier  texto  ubicado  a  la  derecha  de  dos  guiones  (seguidos),  por

                – Este es un comentario

                PUT(SQRT(2.5));  – Este es otro comentario

                El comentario se extiende hasta el final de la línea.

                4. Tipos escalares

                En este capítulo se hechan las bases de los aspectos a pequeñas escala de Ada. Se comienza con la declaración  de  objetos,  la  asignación  de  valores  a  ellos  y  las  idea  de  rango  de  validez  y  visibilidad.Se introducen los importantes conceptos de tipo, subtipos y restricciones.

                4.1. Declaración de objetos y asignaciones

                Los  valores  (datos)  pueden  almacenarse  en  objetos  que  son  de  un  cierto  tipo  (lo  que  se  indica  al momento  de  su  declaración).  Estos  objetos  pueden  ser  variables  o  constantes.  Una  variable  se  declara mediante su nombre (un identificador), su tipo y (posiblemente ) un valor inicial. Por ejemplo:

                I : INTEGER;           — variable de nombre I y de tipo entero

                P: INTEGER := 38; — variable de nombre P, de tipo entero y valor inicial 38

                Es posible declarar varias variables simultáneamente separándolas por comas. Por ejemplo: I,J,K : INTEGER;

                P,Q,R: INTEGER:= 38;

                Si se declara una variable sin un valor inicial es necesario tener cuidado de no usarla con un valor indefinido. Si un programa usa un valor indefinido de una variable no inicializada, su comportamiento puede

                se impredecible. En este caso el programa   debería considerarse estrictamente erróneo, sin embargo tanto el compilador como el sistema de run-time podrían no ser capaces de detectar el problema.

                La manera más simple de asignar un valor a una variable es mediante el comando de asignación (:=). Por ejemplo:

                I:= 36; P:=Q + R;

                Existe una gran similitud entre una declaración con valor inicial y el comando de asignación. Ambos usan := antes de la expresión, la cual puede ser de cualquier nivel de complejidad. Una diferencia importante

                es que es posible inicializar varias variables simultáneamente, pero no es posible usar un único comando de asignación para varias variables. Esto parece poco práctico, sin embargo la necesidad de asignar un mismo valor a varias variables generalmente sólo surge al momento de inicializarlas.

                Una constante se declara de forma similar a una variable, escribiendo la palabra constant después de

                los  dos  puntos.  Lógicamente,  una  constante  debe  ser  inicializada  al  ser  declarada,  de  otro  modo  pierde  su sentido. Por ejemplo:

                PI : constant REAL :=3.1415926536;

                Ejercicio: Indique los errores de las siguientes declaraciones y comandos:

                a)   var I:INTEGER;

                b)  G:constant :=981;

                c)   P,Q:constant INTEGER;

                d)  P:=Q:=7;

                e)   MN:constant INTEGER:=M*N;

                f)   2PI:constant:= 2.0*PI;

                4.2. Bloques y ámbito de validez (scope)

                Ada  hace  una  clara  distinción  entre  la  declaraciones  de  nuevos  identificadores  y  los instrucciones que los usan. Es obvio que las primeras deben preceder a los últimos. Un bloque (block) es la estructura más simple que incluye declaraciones y comandos.

                Un  bloque  comienza  con  la  palabra  reservada  declare,  algunas  declaraciones,  begin,  algunas instrucciones y concluye con la palabra reservada end y un punto y coma. Por ejemplo:

                declare

                I: INTEGER:=0;

                begin

                I:=I+1;

                end;

                Un bloque es un caso particular de una instrucción, por lo tanto una de sus instrucciones   internas puede ser otro bloque.

                Cuando se ejecuta una instrucción “bloque” se prepara su parte declarativa (entre declare y begin) y luego se ejecutan las instrucciones  (entre begin y end). Nótese la terminología: se preparan las declaraciones

                y se ejecutan las instrucciones. La preparación de una declaración consiste en “crear” el objeto declarado y

                asignarle (si corresponde) un valor inicial. Al llegar al final (end) del bloque todas las cosas declaradas en él automáticamente dejan de existir.

                Un  punto  importante  es  que  los  objetos  usados  para  la  inicialización  de  otros  objetos  deben, lógicamente, existir. Es decir, toda declaración debe preceder a su uso. Por ejemplo:

                declare

                I: INTEGER :=0; K: INTEGER:= I;

                begin

                está permitido, pero

                declare

                K: INTEGER:= I; I: INTEGER:= 0;

                begin

                en general no. (¿En qué situación esto estaría correcto, aunque con un significado diferente?)

                Al  igual  que  otros  lenguajes  Ada  maneja  la  idea  de  ocultamiento  (hiding).       Consideremos  el siguiente bloque

                declare

                I,J: INTEGER;

                begin

                . . .                   — aquí es válida la variable I externa

                declare

                I: INTEGER;

                begin

                . . .              — aquí es válida la variable I interna

                end;

                . . .                    — aquí es válida la variable I externa

                end;

                La declaración interna de I no hace que la externa desaparezca, sino que ésta se torna temporalmente invisible. Al terminar el bloque interno, la variable I interna deja de existir y la externa vuelve a estar visible.

                Hay que distinguir entre el ámbito de validez (scope) y la visibilidad de un objeto. Por ejemplo, en el caso  de  un  bloque  el  ámbito  de  validez  de  una  variable  (o  constante)  se  extiende  desde  el  punto  de  su declaración hasta el final (end) del bloque. Sin embargo, una variable (o constante) no es visible en su propia declaración ni en ningún bloque interno donde su nombre sea utilizado para la declaración de otro objeto. Los ámbitos de validez y visibilidad de ilustran en el siguiente ejemplo

                declare

                I: INTEGER:=0;

                begin

                declare

                K: INTEGER:= I; I: INTEGER:= 0;

                begin

                . . .

                end;

                . . .

                end;

                scope visibilidad     scope visibilidad
                I externa I externa     I interna I interna

                Para inicializar K se utiliza el valor de I externa, puesto que la declaración de K precede a la de la

                variable I interna. Vemos pues que el código

                K: INTEGER:= I; I: INTEGER:= 0;

                puede o no ser válido, depende del contexto.

                Ejercicio: Indique qué errores hay en la siguiente sección de código.

                declare

                I: INTEGER:= 7; J,K: INTEGER

                begin

                J:= I+K;

                declare

                P: INTEGER = I; I,J: INTEGER;

                begin

                I:= P+Q; J:= P -Q; K:= I* J;

                end; PUT(K);

                end;

                4.3. Tipos

                Un tipo queda totalmente caracterizado por un conjunto de valores y un conjunto de operaciones. En

                el caso del tipo de datos primitivo INTEGER, el conjunto de valores está representado por

                . . . -3, -2, -1, 0, 1, 2, 3 . . . y las operaciones por

                +, -, *, etc.

                Se asume que los tipos primitivos del lenguaje están definidos en el paquete STANDARD.

                Los valores de un tipo no pueden ser asignados a variables de otro tipo. Esta una regla fundamental del manejo estricto de tipos.

                La declaración de tipos se realiza con una sintaxis distinta a la declaración de variables y constantes para enfatizar la diferencia conceptual existente. La declaración se realiza mediante la palabra reservada type,

                el identificador asociado con el tipo, la palabra reservada is   y la definición del tipo seguida de un punto y coma. Por lo tanto, el paquete STANDARD debería contener declaraciones tales como

                type INTEGER is . . ;

                ejemplo:

                La definición entre is y ; de alguna manera entrega el conjunto de   valores asociados al tipo. Por

                type COLOUR is (RED, AMBER, GREEN);

                Se  ha  definido  un  nuevo  tipo  denominado  COLOUR,  al  que  se  asocia  un  conjunto  de  3  valores: RED, AMBER y GREEN, es decir, las variables de tipo COLOUR sólo podrán “almacenar” alguno de dichos valores. Por ejemplo:

                C: COLOUR;

                D: COLOUR:=RED;

                DEFAULT: constant COLOUR:=GREEN;

                son declaraciones válidas.

                Debido al manejo estricto de tipos no podemos “mezclar” colores y enteros, por ejemplo:

                I: INTEGER; C:COLOUR;

                . . . I := C;

                es incorrecto. Comparemos esto con la “filosofía” del leguaje C (Keninghan, B. Ritchie D. El Lenguaje de

                Programación C).

                Una enumeración es una lista de valores enteros constantes, como en

                enum boolean  {NO, YES};

                El primer nombre en un enum tiene un valor 0, el siguiente 1, y así sucesivamente, a menos que sean especificados  valores  explícitos.   Los  valores  no  especificados  continúan  la  progresión  a  partir  del  último valor que sí lo fue, como en el segundo de esos ejemplos:

                enum escapes  { BELL = ´\a´, RETROCESO = ´\b´, TAB = ´\t´, NVALIN = ´\n´, VTAB = ´\v´, RETURN = ´\r´};

                enum months { ENE = 1, FEB, MAR, ABR, MAY, JUN, JUL, AGO, SEP, OCT, NOV, DIC};

                Las   enumeraciones   proporcionan   una   manera   conveniente   de   asociar   valores   constantes   con nombres,  son   una  alternativa  a  #define.  Aunque  las  variables  de  tipos  enum   pueden  ser  declaradas,  los compiladores  no  necesitan  revisar  que  lo  que  se  va  a  almacenar  en  tal  variable  es  un  valor  válido  para  la enumeración. No obstante, las variables de enumeración ofrecen la oportunidad de revisarlas y tal cosa es a menudo mejor que #define. Además, un depurador puede ser capaz de imprimir los valores de variables de enumeración en su forma simbólica.

                Como puede verse, en Ada existen mecanismos totalmente diferentes para definir constantes y para

                definir tipos de enumeración, en cambio en C los tipos enum sirven para ambos acciones. Además, lo dicho respecto a tipos enum en C es un claro ejemplo de la estilo “relajado” de manejo de tipos de este lenguaje.

                4.4. Subtipos

                Un subtipo, como su nombre lo indica, es un subconjunto de los valores de otro tipo, al que se le denomina tipo base.   Dicho subconjunto se define mediante una restricción. Si embargo no hay forma de restringir el conjunto de operaciones de tipo base.

                Por ejemplo, supongamos que queremos manipular días; sabemos que los días de un mes están en el

                rango de 1 a 31, entonces el subtipo (del tipo INTEGER) sería

                subtype DAY_NUMBER is INTEGER range 1 .. 31;

                Posteriormente  podemos  declarar  variables  y  constantes  usando  el  identificador  de  un  subtipo exactamente igual a como lo hacemos con un identificador de tipo. Por ejemplo, al declarar

                D: DAY_NUMBER;

                estamos asegurando que la variable D pude tomar sólo valores enteros en el rango de 1 a 31. En tiempo de compilación podrían detectarse errores como

                D := 32;

                Además, el compilador agregará  chequeos de tiempo de ejecución (run time checks)  para verificar que se cumpla la restricción, si fallase un chequeo se indicaría un excepción CONSTRAIN_ERROR.

                Es importante notar que la declaración de un subtipo no introduce un nuevo tipo. Un objeto como D

                es de tipo entero y por lo tanto el siguiente código es totalmente legal

                D: DAY_NUMBER;

                I: INTEGER;

                . . . D:=I;

                Lógicamente,  durante  la  ejecución,  el  valor  de  I  podría  salirse  del  rango  1  a  31.  En  este  caso  se indicaría CONSTRAIN_ERROR. Pero una asignación en el sentido inverso

                I := D;

                siempre funcionará correctamente.

                No es necesario declarar un  nuevo tipo para imponer una restricción. Por ejemplo, mediante

                D: INTEGER range 1 .. 31;

                se ha declara una variable de tipo entero junto con una restricción. Ejercicio:

                1. ¿Cuándo tendrá sentido definir un tipo con una cierta restricción  y cuándo una variable?

                2. ¿Qué diferencia semántica habrá entre las siguientes declaraciones?

                a) subtype DAY_NUMBER is INTEGER range 1 .. 31;

                b) type DAY_NUMBER is range 1 .. 31;

                4.5. Tipos numéricos simples

                La complejidad de los problemas de análisis numérico (estimación de errores y otros) se refleja en

                los tipos de datos numéricos de   Ada. Sin embargo, aquí expondremos sólo los aspectos más simples de los tipos entero (INTEGER) y real (REAL), para usarlos en nuestros ejemplos.

                El   valor   mínimo   del   tipo   INTEGER   está   dado   por   INTEGER´FIRST   y   el   máximo   por

                INTEGER´LAST. Estos son dos ejemplos de atributos, los cuales se denotan mediante un apóstrofe seguido

                de un identificador.

                El  valor  de  INTEGER´FIRST  dependerá  de  la  implementación  y  siempre  será  negativo.  En  una máquina de 16 bits (con complemento a dos) se tendrá

                INTEGER´FIRST = -32768

                INTEGER´LAST = +32767

                Por supuesto que para lograr una mejor portabilidad debemos usar los atributos FIRST y LAST en lugar de los valores -32768 y +32767. Dos subtipos muy útiles son

                subtype NATURAL is INTEGER range 0 .. INTEGER´LAST;

                subtype POSITIVE is INTEGER range  1 .. INTEGER´LAST;

                Los atributos FIRST y LAST se aplican automáticamente a los subtipos, por ejemplo

                POSITIVE´FIRST = 1

                NATURAL´LAST = INTEGER´LAST

                También  es  posible  hacer  restricciones  sobre  los  reales.  Además,    los  atributos  FIRST  y  LAST

                también son aplicables.

                A continuación se resumen algunas de las operaciones predefinidas para los tipos entero y real más importantes.

                1)  +, - Estos son tanto operadores unarios como binarios. En el primer caso el operando puede ser entero o real; el resultado será del mismo tipo. El operador unario + en realidad no hace nada, pero  el  operador  unario  -  cambia  el  signo  del  operando.  Cuando  se  usan  como  operadores binarios, ambos operandos deben ser del mismo tipo y el resultado será del tipo que corresponda.

                Su acciones son la adición y sustracción  normales.

                2)  *     Multiplicación;  ambos  operandos  deben  ser  enteros  o  reales;  el  resultado  será  del  tipo correspondiente.

                3)  /  División;  ambos operandos deben ser del mismo tipo. La división entero trunca el resultado.

                4)  rem   Resto; los operandos deben ser enteros y el resultado también lo es. Entrega el resto de la división.

                5)  mod  Módulo; ambos operandos deben ser enteros y el resultado también lo es. Corresponde a la operación matemática módulo.

                6)  abs   Valor absoluto; es un operador unario y el operador debe ser entero o real. El resultado es del mismo tipo y corresponde al valor absoluto del operando.

                7)  **  Exponenciación; eleva el primer operando a la potencia indicada en el segundo. Si el primer operando  es  un  entero,  entonces  el  segundo  debe  ser  un  entero  positivo  o  cero.  Si  el  primer operando es un real, entonces el segundo puede ser cualquier entero. El resultado es del mismo tipo del primer operando.

                Además,  pueden  realizarse  las  operaciones   =,   /=,  <,  <=,  >  y >=  para  obtener  resultados  de  tipo booleano TRUE o FALSE. También en este caso los operadores deben ser del mismo tipo.

                A pesar que estas operaciones son bastante sencillas es necesario hacer algunas observaciones.

                La regla general es que no se puede mezclar valores de distinto tipo en una expresión aritmética. Por ejemplo, no se puede sumar un entero a un real. El cambio de un valor INTEGER a uno REAL  o viceversa puede hacerse usando el nombre del tipo (o subtipo) de dato que se necesita como si fuera el nombre de una función.   Por ejemplo, si tenemos

                I: INTEGER:=3; R:REAL:=5.6;

                no podemos escribir

                I + R

                pero si es válida la expresión

                REAL(I) + R

                que usa la “suma real” para entregar 8.6, o

                I + INTEGER(R)

                que entrega 9, al hacer uso de la “suma entera”.

                La conversión de real a entero siempre entrega un valor redondeado,  por ejemplo:

                1.4 entrega 1

                1.6 entrega 2

                Dependiendo  de  la  implementación  los  valores  intermedios  (como  1.5)  serán  redondeados  hacia arriba o hacia abajo.

                Finalmente, hagamos algunas observaciones respecto al operador **. Para un exponente positivo (es decir, un entero positivo) esta operación corresponde a una multiplicación repetida. Por ejemplo:

                3**4 = 3*3*3*3 = 81

                3.0**4 = 3.0*3.0*3.0*3.0* = 81.0

                Cuando el segundo operando es cero, el resultado será obviamente uno

                3**0 = 1

                3.0**0 = 1.0

                El exponente no puede ser negativo si la base es entera, ya que el resultado podría no ser entero. Pero si la base es real este operador entrega el correspondiente recíproco

                3.0**(-4) = 1.0/81.0 = 0.0123456780123…

                Como es usual existen diferentes niveles de precedencia entre los operadores en una expresión, y, además, la precedencia natural puede ser alterada con el uso de paréntesis. Los operadores de igual jerarquía son aplicados de izquierda a derecha. La jerarquía de operados en Ada es la siguiente

                =  /=  <  <=  >  >=

                +   -                        (binarios)

                +  -                          (unarios)

                / * mod rem
                abs **
                Por ejemplo:
                A/B*C significa (A/B)*C
                A+B*C+D significa A+(B*C)+D
                A*B+C*D significa (A*B)+(C*D)
                A*B**C significa A*(B**C)

                Como ya se indicó, por omisión los operadores de un mismo nivel se aplican de izquierda a derecha,

                sin embargo, no está permitido escribir varios operadores de exponenciación sin el uso de paréntesis. Por esto

                la expresión

                A**B**C

                es incorrecta y debe escribirse

                (A**B)**C          o             A**(B**C)

                De esta manera se evita el riesgo de que accidentalmente se escriba una expresión sintácticamente correcta, pero semánticamente errónea.

                Es necesario tener cuidado con los operadores unarios, por ejemplo:

                -A**B      significa      -(A**B)      y no     (-A)**B

                además, las expresiones

                A**-B   y  A*-B

                son ilegales. Es necesario, en estos casos, utilizar paréntesis para definir claramente lo que se desea calcular. Ejecicio:

                1. Evalúe las siguientes expresiones:

                I:INTEGER:=7; J:INTEGER:=-5; K:INTEGER:=3;

                a) I*J*K b) I/J*K c) I/J/K

                d) J+2 rem I

                e) K**K**K

                2. Escriba las siguientes expresiones matemáticas en Ada. Defina identificadores apropiados.

                a) b2 - 4ac

                b) p r3

                4.6. Tipos de enumeración

                Primero mostremos algunos ejemplos de tipos de enumeración:

                type COLOUR is (RED,AMBER,GREEN);

                type  DAY is (MON, TUE, WED, THU, FRI, SAT, SUN);

                type STONE is (BERYL, QUARTZ);

                type SOLO is (ALONE);

                No existe un límite superior para la cantidad de valores de un tipo de enumeración, pero debe haber

                al menos uno.

                Las restricciones de los tipos enumerados son semejantes a las restricciones sobre los enteros. Por

                ejemplo:

                subtype WEEKDAY is DAY range MON .. FRI;

                D: WEEKDAY;

                Si el límite superior es inferior al límite inferior se obtendrá un rango vacío. Por ejemplo:

                subtype COLOURLESS is COLOUR range AMBER .. RED;

                Los atributos FIRST y LAST también son aplicables a los tipos de enumeración, entonces

                COLOUR´FIRST = RED WEEKDAY´LAST = FRI

                Otros atributos predefinidos entregan el sucesor (SUCC) y el predecesor (PRED) de un cierto valor

                de un tipo de enumeración. Por ejemplo:

                COLOUR´SUCC(AMBER) = GREEN STONE´SUCC(BERYL) = QUARTZ DAY´PRED(FRI) = THU

                Lógicamente, lo indicado entre paréntesis es cualquier expresión válida del tipo correspondiente. Si

                se  intenta  obtener  el  antecesor  (sucesor)  del  primer  (último)  valor  el  sistema  entregará  una  excepción

                CONSTRAIN_ERROR.

                Otro atributo predefinido   es POS, el cual entrega la posición del valor dentro de la declaración del tipo (empezando desde cero). El atributo contrario a POS es VAL, que entrega el valor asociado a una cierta posición. Por ejemplo:

                COLOUR´POS(RED) = 0

                COLOUR´POS(AMBER) = 1

                COLOUR´POS(GREEN) = 2

                COLOUR´VAL(0) = RED DAY´VAL(6) = SUN

                SOLO´VAL(1)   – CONSTRAIN_ERROR

                Ada  incluye  los  atributos  POS  y  VAL,  puesto  que  podrían  ser  necesarios  en  algunas  situaciones, pero no se recomienda su uso pues no es una buena práctica de programación. Es mejor pensar en términos de valores de enumeración que en números.

                Puesto que se establece un orden entre los valores de un tipo de enumeración , es posible aplicar los operadores  =, /=, <, <=, > y  >=. Por ejemplo:

                RED < GREEN   es   TRUE

                WED >= THU      es   FALSE

                Ejercicio:

                a)   Evalúe

                1) DAY´SUCC(WEEKDAY´LAST)

                2) WEEKDAY´SUCC(WEEKDAY´LAST)

                3) STONE´POS(QUARTZ)

                b)  Declare los siguientes tipos

                1) frutas típicas.

                2) proveedores de computadores.

                c)   Si el primer día del mes está  en la variable D de tipo DAY, escriba una asignación   que                reemplace

                D por el día de la semana del N-ésimo día del mes.

                4.7. Tipo Booleano

                El tipo booleano es un tipo de enumeración predefinido, cuya declaración puede considerarse como

                type  BOOLEAN is (FALSE, TRUE);

                Los  valores  booleanos  son  producidos  por  los  operadores  =,  /=,  <,  <=,  >  y  >=,  los  que  tienen  el significado normalmente aceptado y se aplican a muchos tipos. Por ejemplo:

                if TODAY = SUN then

                TOMORROW := MON;

                else

                end if;

                TOMORROW:= DAY´SUCC(TODAY);

                Existen otros operadores asociados al tipo BOOLEAN.

                1.   not  Es un operador unario y cambia TRUE a FALSE y viceversa.

                2.   and  Es un operador binario. El resultado es TRUE si ambos operadores son TRUE, en otro         caso será

                FALSE.

                3.   or Es un operador binario. El resultado es TRUE si al menos uno de los operadores es TRUE, es FALSE

                si ambos operadores son FALSE.

                4.   xor Es un operador binario. El resultado es TRUE si y sólo si ambos operadores son distintos, es decir, uno es TRUE y el otro es FALSE.

                Los  operadores   and,  or  y  xor  tienen  la  misma  precedencia,  la  cual  es  menor  que  cualquier  otro operador. En particular, tienen menor precedencia que =, /=, <, <=, > y >=. Como consecuencia de esto no se necesitan paréntesis para expresiones como

                P < Q and  I = J

                Sin  embargo,  a  pesar  de  tener  la  misma  precedencia,  los  operadores  and,  or  y  xor  no  pueden mezclarse sin paréntesis, entonces

                B and C or D

                es incorrecto y debe escribirse

                B and (C or D)       o          (B and C) or D

                para enfatizar el sentido de la expresión.

                Los programadores familiarizados con otros lenguajes recordarán que los operadores and y or tienen distinta precedencia. Esto puede conducir a errores, por ello Ada les da la misma precedencia y obliga a usar paréntesis. Obviamente, si se aplica el mismo operador en forma sucesiva no es necesario usar paréntesis ya que tanto and como or son asociativos. Por ejemplo:

                B and C and D

                es una expresión correctamente escrita.

                Las  variables  y  constantes  booleanas   pueden  ser  declaradas  y  utilizadas  en  la  manera  usual.  Por

                ejemplo:

                DANGER: BOOLEAN;

                SIGNAL: COLOUR;

                . . .

                DANGER := SIGNAL = RED;

                La variable DANGER será TRUE si la señal es RED. Además, podríamos escribir

                if DANGER then

                STOP_TRAIN;

                end if;

                Notemos que no deberíamos escribir

                if DANGER = TRUE then

                porque a pesar que es sintácticamente correcto no toma en cuenta que DANGER es una variable booleana y puede utilizarse directamente como una condición. Peor aún sería escribir

                if  SIGNAL = RED then

                DANGER .= TRUE;

                else

                end if;

                DANGER := FALSE;

                en lugar de           DANGER := SIGNAL = RED;

                Ejercicio:

                1.   Escriba declaraciones de constantes T y F  con los valores TRUE y FALSE.

                2.   Evalúe  (A /= B) = (A xor B) para todos los valores posibles de las variables booleanas A y B.

                5. Estructuras de Control

                Ada  tiene  tres  estructuras  de  control:  if,  case  y  loop.  A  pesar  que  estas  tres  estructuras  son suficientes para escribir programas con claridad, el lenguaje también incluye la instrucción goto.

                La sintaxis de estas estructuras de control es similar. Existe una palabra reservada de inicio: if, case

                o loop, la que es pareada al final de la estructura por la misma palabra reservada precedida por la palabra end. Entonces tenemos

                if                            case                        loop

                …                            …                           …

                end if;                   end case,               end loop;

                La palabra loop puede ir precedida por un cláusula de iteración que comienza con for o while.

                5.1. Instrucción if

                La sintaxis de esta instrucción es:

                if_statement ::= if condition  then

                sequence_of_statements

                {elseif condition then

                sequence_of_statements}

                [else

                end if;

                sequence _of_statements]

                condition::= boolean_expression

                La semántica de la instrucción if de Ada es semejante a sus análogas en otros lenguajes. Veamos algunos ejemplos:

                if HUNGRY then COOK; EAT; WASH_UP;

                end if;

                Esta es la variante más simple de la instrucción if. En este ejemplo la expresión booleana (condition) está dada por una variable booleana (HUNGRY) y las instrucciones (sequence_of_statements) son llamadas a procedimientos.

                Las  instrucciones  internas  al  if  (tanto  en  la  rama  then  como  en  la  else)  pueden  tener  cualquier complejidad, incluso pueden ser instrucciones if anidadas. Por ejemplo, si deseamos encontrar las raíces de

                ax2  + bx + c = 0

                Lo primero que hay que chequear es el valor de a. Si a = 0 la ecuación cuadrática se transforma en una ecuación lineal con una única raíz igual a -c/b. Si a no es cero se debe analizar el discriminante b2-4ac para  ver si las raíces son reales o complejas.  Esto podría programarse de la siguiente manera

                if A = 0.0 then

                – caso lineal

                else

                if B**2 - 4.0*A*C  >= 0.0 then

                – raíces reales

                else

                – raíces complejas

                end if;

                end if;

                Obsérvese la repetición de end if. Esto no es muy estético y ocurre con la suficiente frecuencia para

                ameritar una construcción especial: elseif, cuyo uso se muestra a continuación

                if A = 0.0 then

                – caso lineal

                elseif B**2 - 4.0*A*C >= 0.0 then

                – raíces reales

                else

                end if;

                – raíces complejas

                De  esta  forma  se  enfatiza  el  mismo  status  de  los  tres  casos  y  la  naturaleza  secuencial  de  los

                chequeos. La rama elseif puede repetirse un número arbitrario de veces y la rama final else es opcional. Ejercicio:

                a) Las variables DAY, MONTH y YEAR contienen el día en curso, y están declaradas de la siguiente manera.

                DAY: INTEGER range 1 .. 31; MONTH: MONTH_NAME;

                YEAR: INTEGER range 1901 .. 2099;

                donde

                type MONTH_NAME IS (JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT, NOV,DEC);

                Escriba una sección de programa Ada que determine la fecha del día siguiente ¿Qué ocurrirá                  si la fecha es 31 DEC 2099?

                b)  X  e  Y  son  dos  variables  reales.  Escriba  un  bloque  que  intercambie  sus  valores,  si  es  necesario,  para asegurar que el valor mayor quede en X.

                5.2. Instrucción Case

                Una  instrucción  case  permite  elegir  una  secuencia  de  instrucciones  de  entre  varias  alternativas  de acuerdo  al  valor  de  una  expresión.  Por  ejemplo,  para  controlar  los  movimientos  de  un  vehículo  robot podríamos utilizar

                case ORDER is

                when LEFT => TURN_LEFT; when RIGHT => TURN_RIGHT; when BACK => TURN_BACK; when ON => null;

                end case;

                Es necesario explicitar todos los valores posibles (sólo una vez) para asegurar que no haya omisiones accidentales. Si para uno o más valores no se requiere una acción específica (como en el ejemplo) se debe usar la instrucción null, la cual no hace absolutamente nada, pero indica que eso es precisamente lo que se desea.

                Es  frecuente  que  se  necesite  realizar  una  misma  acción  para  varios  valores.  Por  ejemplo,  para

                expresar la idea que desde lunes a jueves se trabaja; el viernes se trabaja y se asiste a una fiesta; y los sábados

                y domingos no hay actividades fijas, podemos escribir

                case TODAY is

                when MON|TUE|WED|THU => WORK;

                when FRI                                    => WORK;

                PARTY;

                when SAT|SUN                          =>  null;

                end case;

                Si para varios valores sucesivos se realiza una misma acción, se pueden usar rangos, en el ejemplo anterior podría haberse escrito

                when MON .. THU => WORK;

                A  veces  se  requiere  que  una  cierta  acción  se  realice  para  todos  los  valores  que  no  hayan  sido explícitamente indicados; esto se realiza utilizando la palabra reservada others, la cual puede aparecer sólo en

                la última alternativa. Según esto, el ejemplo anterior podría reescribirse

                case TODAY is

                when MON|TUES|WED|THU => WORK; when FRI => WORK; PARTY;

                when  others                               =>  null;

                end case;

                Ejercicio: Reescriba la respuesta al ejercicio (a) de la pág. anterior usando la instrucción case.

                5.3. Instrucciones de iteración

                La forma más simple de una instrucción de iteración es

                loop

                sequence_of_statements

                end loop;

                Las instrucciones internas a la iteración se repiten indefinidamente hasta que una de ellas termina el loop de alguna manera.  Por ejemplo, consideremos el cálculo del número trascendental e.

                e = 1 + 1/1! + ½! + 1/3! + ¼!  +  . . . Una posible solución sería

                declare

                begin

                end;

                E:REAL := 1.0; I:INTEGER := 0; TERM:REAL :=1.0;

                loop

                I:=I+1;

                TERM:= TERM / REAL(I); E:=E + TERM;

                end loop;

                . . .

                Notemos  que  la  variable  I  es  de  tipo  entero  porque  la  lógica  del  problema  nos  indica  que  es  un

                contador, pero el valor de e a calcular debe ser real, por eso es necesario hacer el cambio de entero a real al calcular el nuevo valor de TERM en cada iteración.

                Matemáticamente hablando la serie arriba indicada tiende al valor de e cuando n tiende a infinito. Sin embargo, al realizar el cálculo computacional no es posible desarrollar la serie hasta el infinito, puesto que así nunca obtendríamos el valor requerido y, por otra parte, sabemos que la representación de los reales

                en  un  computador  no  es  exacta,  por  lo  que  tampoco  tiene  sentido  desarrollar  la  serie  hasta  un  valor  muy grande de n.  La forma más sencilla de finalizar el loop es determinar una  cantidad fija  iteraciones antes del inicio del loop.  Por ejemplo, supongamos que se decide realizar el loop N veces (N puede ser una constante

                o una variable a la que de alguna manera se le asigna un valor antes del loop), éste quedaría

                loop

                if I = N then exit; end if; I:=I+1;

                TERM:= TERM / REAL(I); E:=E + TERM;

                end loop;

                La   instrucción   exit   detiene   el   proceso   de   iteración,   pasando   el   control   a   la   instrucción inmediatamente siguiente a end loop. Puesto que la construcción

                if condition then exit; end if;

                es bastante usada, el lenguaje provee una construcción análoga más abreviada

                exit when condition;

                Por lo que el loop quedaría

                loop

                exit when I = N; I:=I+1;

                TERM:= TERM / REAL(I); E:=E + TERM;

                end loop;

                La  instrucción  exit  puede  aparecer  en  cualquier  parte  del  loop:  al  principio,  al  medio  o  al  final. Además, puede aparecer más de una vez (en distintas ramas de instrucciones if, posiblemente, anidadas), sin embargo se recomienda estructurar el loop de forma tal que exista una sola sentencia exit dentro de cada loop.

                Es bastante común que la instrucción vaya al comienzo del loop (antes de cualquier otra instrucción) como en

                nuestro ejemplo. Para estos casos se puede usar la palabra reservada while, de la siguiente manera

                while I /= N loop

                I:=I+1;

                TERM:= TERM / REAL(I); E:=E + TERM;

                end loop;

                En  los  casos  en  que  se  requiere  iterar  un  número  específico  de  veces  se  puede  usar  una  última variante de la instrucción loop, la que consiste en la utilización de la palabra reservada for. Nuestro ejemplo quedaría

                for  I in 1 .. N loop

                TERM:= TERM / REAL(I); E:=E + TERM;

                end loop;

                La variable usada para controlar la iteración (en este caso I) se declara implícitamente y no requiere

                ser  definida  externamente.  Su  tipo  se  determina  por  el  tipo  indicado  en  el  rango  de  variación,  y  para  los efectos internos del loop debe considerarse como una constante, en el sentido en que en cada iteración tiene

                un  valor  (determinado  por  el  mecanismo  del  loop)  que  no  puede  ser  modificado.  Cuando  se  termina  la iteración (por el término del rango o por una instrucción exit) I deja de existir.

                Para tomar los valores del rango en orden descendente se utiliza la palabra reservada reverse, de la manera siguiente

                for I in reverse 1 .. N loop

                Nótese que el rango  siempre se escribe en forma ascendente. No es posible definir un paso distinto

                de 1. Esto no debería causar problema porque la gran mayoría de los ciclos controlados por una variable lo hacen  con  un  paso  de  1  (ascendente  o  descendente)  y  aquellos  que  tienen  un  paso  diferente  (que  son  los menos) pueden ser simulados agregando los factores correspondientes. Por ejemplo, para calcular

                n

                å 1 / 2i

                i =1

                el código Ada más adecuado sería,

                for  I in 1 .. N loop

                TERM:= 1.0 / (2.0 * REAL(I)); E:=E + TERM;

                end loop;

                ya que refleja mejor la notación matemática estandar.

                El rango en un loop tipo for puede ser vacío (por ejemplo si N es cero en el caso anterior) en cuyo caso  el  loop  termina  en  forma  inmediata.  Los  límites  de  un  rango  no  necesariamente  son  constantes  y/  o variables, son en general expresiones de un cierto tipo discreto (obviamente, el mismo para ambos límites). Las expresiones que determinan los límites son evaluadas una sola vez antes del inicio de la primera iteración

                y no pueden ser cambiados dentro del loop. Por este motivo el loop

                N:=4;

                for I in 1 .. N loop

                . . . N:=10;

                end loop;

                será ejecutado 4 veces a pesar que el valor de N haya sido cambiado a 10.

                Como ya se dijo, el rango debe ser  de algún tipo discreto, es decir, no necesariamente de tipo entero. Por ejemplo, para realizar una cierta acción para cada uno de los días de la semana escribiríamos

                for TODAY in DAY loop

                . . .

                end loop;

                si quisieramos  sólo realizar la acción para los días lunes a viernes escribiríamos

                for TODAY in MON … FRI  loop

                . . .

                end loop;

                pero una manera más adecuada sería

                for TODAY in WEEKDAY  loop

                . . .

                end loop;

                Como ya dijimos, la instrucción exit pasa el control al punto inmediatamente siguiente al loop dentro

                del cual se encuentra. Pero los   loops pueden estar anidados y a veces ocurre que se desea salir de toda la estructura  anidada  y  no  solo  del  loop  más  interno.  Por  ejemplo,  supongamos  que  estamos  realizando  una búsqueda en dos dimensiones.

                for I in 1 .. N loop

                for J in 1 .. M loop

                – si los valores de I y J satisfacen una cierta

                – condición terminar la búsqueda

                end loop;

                end loop;

                Una  instrucción  exit  simple  nos  sacaría  del  loop  interno,  pero  estaríamos  obligados  a  chequear nuevamente  la  condición  (agreguese  el  inconveniente  de  que  J  ya  no  existe)  inmediatamente  después  de terminado el loop interno. Este problema puede ser resuelto dando un nombre al loop externo y usando dicho nombre para salir inmediatamente de ambos.

                SEARCH:

                for I in 1 .. N loop

                for J in 1 .. M loop

                if condition_O_K then I_VALUE :=  I; J_VALUE := J;

                exit SEARCH;

                end if;

                . . . end loop;

                end loop SEARCH;

                — el control pasa a este punto

                Un loop es bautizado colocando un identificador antes de su inicio seguido de dos puntos. (Esto se parece mucho a la forma en que en otros lenguajes se definen labels, pero en Ada no es así, ya que no es posible  usar  los  nombres  de  loops  para  la  instrucción  Goto.)  El  identificador  debe  ser  repetido  entre  el correspondiente end loop y el punto y coma final.

                La instrucción exit  en su forma condicional también puede referenciar a un loop por su nombre.

                exit SEARCH when condition;

                Ejercicio:

                a) Calcule

                n

                1

                g = å p  - ln n
                p=1

                n ® ¥, g ® g

                = 0.577215665

                La función Ada LN entrega el logaritmo natural, el parámetro debe ser real y el resultado también es real.

                b)  Calcule  e  con  un  error  absoluto  de  0.00005.  Es  decir,  la diferencia  en  valor  absoluto  entre  dos  valores

                (calculados) de e consecutivos debe ser menor o igual a 0.00005.

                5.4. Instrucción goto y labels

                A muchos podría sorprender el que un lenguaje de programación moderno contemple la instrucción goto, puesto que su uso  se ha considerado una mala práctica de programación. Ada incluye esta instrucción, pero no estimula su uso al entregar estructuras de control suficientemente variadas para expresar cualquier flujo de control normalmente usado.

                La  generación  automática  de  programas  es  el  motivo  principal  para  incluir  la  instrucción  goto  en

                Ada. Cuando se genera automáticamente un programa Ada desde una cierta especificación de más alto nivel, ésta última será considerada como “el programa fuente”, por lo que el código Ada no tiene porque ser muy legible y es posible realizar cierta licencias, entre ellas el uso del goto. Además, al traducir un programa desde Cobol o Fortran a  Ada  puede resultar conveniente usar goto para hacer una traducción literal del programa original.

                La instrucción goto en Ada tiene una sintaxis semejante a la de otros lenguajes, la palabra reservada

                goto seguido de un nombre de label, el cual se define entre paréntesis angulares dobles. Por ejemplo el label

                de nombre INICIO se define

                <<INICIO>>

                colocándolo  en  el  lugar  donde  se  desea  que  continúe  el  control  del  flujo  de  ejecución  del  programa.  La instrucción en sí tendrá la siguiente forma

                goto  INICIO;

                Una instrucción goto no puede ser usada para transferir el control al interior de un if, case o loop, ni entre las ramas de las instrucciones if o case.

                6. Tipos Compuestos

                En esta sección se describirán los tipos compuestos, es decir, arreglos y registros. Además, con la introducción de los caracteres y strings  se completará la discusión sobre los tipos de enumeración.

                6.1. Arreglos

                Un arreglo es un objeto compuesto que consiste de un grupo de componentes todos del mismo tipo. Un arreglo puede ser de una, dos o más dimensiones. Una declaración típica de un arreglo es

                A: array (INTEGER range 1..6) of REAL

                Aquí se declara la variable A como un objeto que contiene 6 componentes, cada uno de los cuales es

                de tipo REAL. Para referirse a los componentes individuales se debe indicar el nombre del array seguido de una expresión entre paréntesis que entrega un valor entero dentro del rango discreto 1..6. Si el resultado de esta expresión, conocida como el valor del índice, no está dentro del rango especificado, el sistema emitirá la excepción CONSTRAIN_ERROR. Para colocar ceros en cada componente de A podríamos escribir

                for I in 1..6 loop

                A(I) := 0.0;

                end loop;

                Como se dijo, un arreglo puede tener varias dimensiones, en cuyo caso se debe indicar un rango para cada dimensión. Entonces

                AA: array (INTEGER range 0..2, INTEGER range 0..3) of REAL;

                es  un  arreglo  de  12  componentes,  cada  uno  de  los  cuales es  referenciado  mediante  dos  índices  enteros,  el primero  dentro  del  rango  0..2  y  el  segundo  en  el  rango  0..3.  Para  colocar  todos  estos  elementos  en  cero podríamos escribir

                for I in 0 .. 2 loop for J in 0 .. 3 loop AA(I,J) := 0.0; end loop;

                end loop;

                Los rangos no deben ser necesariamente estáticos, por ejemplo, es posible escribir

                N: INTEGER:= . . .;

                . . .

                B: array (INTEGER range 1..N) of BOOLEAN;

                así, la cantidad de componentes de B estará determinada por el valor N, el que lógicamente deberá tener un valor antes de realizar la elaboración de B.

                Los rangos discretos en los arreglos siguen reglas similares a los rangos de los sentencias for. Una

                de ellas es que un rango de la forma 1..6 implica que el tipo asociado es INTEGER, de ahí que podamos escribir

                A: array (1..6) of REAL;

                ejemplo

                Sin  embargo,  el  índice  de  un  arreglo  puede  ser  de  cualquier  tipo  discreto.  Podríamos  tener  por

                HOURS_WORKED: array (DAY) of REAL;

                Este       arreglo       tiene       siete       componentes,      desde       HOURS_WORKED(MON)         hasta HOURS_WORKED(SUN). Podemos usar este arreglo para almacenar las horas de trabajo para cada día de la semana

                for D in WEEKDAY loop

                HOURS_WORKED(D) := 8.0; end loop; HOURS_WORKED(SAT) := 0.0; HOURS_WORKED(SUN) := 0.0;

                Si  quisiésemos  que  el  arreglo  sólo  tuviera  cinco  elementos,  uno  para  cada  día  de lunes a viernes, podríamos escribir

                HOURS_WORKED: array (DAY range MON..FRI) of REAL;

                o mejor

                HOURS_WORKED: array (WEEKDAY) of REAL;

                Los  arreglos  tienen  varios  atributos  relacionados  con  sus  índices.  A’FIRST  y  A’LAST  entregan, respectivamente,   los límites inferior y superior del primer (o único) índice de A. Entonces, de acuerdo a la última declaración de HOURS_WORKED

                HOURS_WORKED’FIRST = MON HOURS_WORKED’LAST = FRI

                A’LENGTH entrega la cantidad de valores del primer (o único) índice de A.

                HOURS_WORKED’LENGTH = 5

                A’RANGE es una forma abreviada para A’FIRST .. A’LAST. Entonces

                HOURS_WORKED’RANGE = MON .. FRI

                Los mismos atributos pueden aplicarse a los demás índices de un arreglo multidimensional, para ello

                se  debe  agregar  la  dimensión  (una  expresión  entera  estática)  involucrada  entre  paréntesis.  Entonces,  en  el caso de nuestro arreglo bidimensional AA tenemos

                AA’FIRST(1) = 0

                AA’FIRST(2) = 0

                AA’LAST(1) = 2

                AA’LAST(2) = 3

                AA’LENGHT(1) = 3

                AA’LENGHT(2) = 4

                AA’RANGE(1) = 0 .. 2

                AA’RANGE(2) = 0 .. 3

                El atributo RANGE es particularmente útil con iteraciones. Nuestros ejemplos anteriores quedarían mejor escritos como

                for I in A’RANGE loop

                A(I) := 0.0;

                end loop;

                for I in AA’RANGE(1) loop

                for J in AA’RANGE(2) loop

                AA(I,J) := 0.0;

                end loop;

                end loop;

                El atributo RANGE también puede utilizarse en declaraciones, por ejemplo

                J: INTEGER range A’RANGE;

                es equivalente a

                J: INTEGER range 1 .. 6;

                En  lo  posible,  es  mejor  utilizar  los  atributos  para  reflejar  las  relaciones  entre  entidades  de  un programa, de este modo los cambios quedan restringidos a las declaraciones sin que, en general, impliquen

                cambios  en  el  resto  del  código.  Por  ejemplo,  si  cambiamos  los  rangos  de  los  arreglos  A  y  AA,  no  será necesario modificar los ciclos for usados para asignarles valores.

                Los arreglos que hemos visto hasta el momento son variables en el sentido normal. Por lo tanto, se

                les puede asignar valores y ser usadas en expresiones. Al igual que otras variables, a los arreglos se les puede asignar un valor inicial. Esto generalmente se denota mediante un conjunto que es la notación literal de un arreglo.  La  forma  más  simple  de  realizar  esta  asignación  es  mediante  una  lista  ordenada  (encerrada  entre paréntesis) de expresiones (separadas por comas) que entregan los valores de los componentes. Por ejemplo, para inicializar el arreglo A con ceros, escribimos

                A: array (1 .. 6) of REAL := (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);

                En el caso de arreglos multidimensionales la lista será una lista con sublistas, por ejemplo

                AA: array (0 .. 2, 0 .. 3) of REAL := ( (0.0,  1.0,  2.0,      3.0), (4.0,  5.0,  6.0,    7.0),

                (8.0,  9.0,  10.0, 11.0) ); De acuerdo a esta declaración, tenemos

                AA(0,0) = 0.0

                AA(0,1) = 1.0

                AA(0,2) = 2.0

                AA(0,3) = 3.0

                AA(1,0) = 4.0

                etc.

                Nótese que la lista debe estar completa, es decir, si se desea inicializar un componente del arreglo, entonces deben inicializarse todos, y en el orden correcto.

                Es posible declarar un arreglo como constante, en cuyo caso, obviamente, es obligatorio dar un valor inicial.  Este  tipo  de  arreglos  es  útil  para  implementar  tablas.  En  el  siguiente  ejemplo  se  usa  un  arreglo constante para determinar si un día en particular es día de trabajo o no.

                WORK_DAY: constant array (DAY) of BOOLEAN := (TRUE, TRUE, TRUE, TRUE, TRUE, FALSE, FALSE);

                Un ejemplo interesante lo constituye un arreglo constante que permite determinar el día siguiente, sin preocuparse del último día de la semana.

                TOMORROW: constant array (DAY) of DAY := (TUE, WED, THU, FRI, SAT, SUN, MON); Finalmente,  notemos  que  los  componentes  de  un  arreglo  pueden  ser  de  cualquier  tipo  o  subtipo.

                Además, las dimensiones de un arreglo multidimensional pueden ser de diferentes tipos discretos. Un ejemplo

                un tanto extraño, pero válido, sería

                STRANGE: array (COLOUR, 2 .. 7, WEEKDAY range TUE .. THU)

                of PLANET range MARS .. SATURN;

                Ejercicio:

                1. Declare un arreglo F de enteros cuyo índice varíe entre 0 y N. Además, escriba código Ada para asignar a los componentes de F su correspondiente valor de la serie de Fibonaci.

                F0 = 0,  F1 = 1, Fi = Fi-1 + Fi-2

                2. Escriba código Ada para encontrar los índices I y J de componente con mayor valor del arreglo

                A:array (1 .. N, 1 .. M) of REAL;

                3. Declare un arreglo DAYS_IN_MONTH que permita determinar el número de días de cada mes.

                4. Declare un arreglo YESTERDAY análogo al arreglo TOMORROW dado en un ejemplo anterior.

                5. Declare un arreglo BOR tal que:   BOR(P,Q) = P or Q

                6. Declare una matriz constante unitaria UNIT de orden 3. Una matriz unitaria es aquella que tiene unos en su diagonal principal y ceros en todos los demás componentes.

                6.2. Tipos arreglo

                Los  arreglos  hasta  ahora  vistos  no  tienen  un  tipo  explícito.  De  hecho  son  de  tipo  ”anónimo”. Reconsiderando el primer ejemplo del punto anterior, podemos escribir la   definición de tipo

                type VECTOR_6 is array (1 .. 6) of REAL;

                y luego declarar  A usando el tipo definido  de la manera ya conocida

                A: VECTOR_6;

                Una ventaja de usar tipos es que es posible realizar asignaciones de arreglos completos que han sido declarados separadamente.  Por ejemplo, si además declaramos

                B:VECTOR_6;

                podemos escribir

                B:= A;

                que sería una forma abreviada y más recomendable de

                B(1):= A(1); B(2):= A(2); . . .B(6):= A(6); Por otro lado, si escribimos

                C: array (1 .. 6) of REAL; D: array (1 .. 6) of REAL;

                la expresión C:= D; será ilegal debido a que C y D no son del mismo tipo. Son de diferentes tipos, ambos anónimos. La regla que subyace en todo esto es que cada definición de tipo introduce un nuevo tipo y

                en este caso la sintaxis nos dice que una definición de tipo arreglo es el trozo de texto que comienza con la palabra array hasta el punto y coma (exclusive). Incluso, si escribimos

                C,D: array (1 .. 6) of REAL;

                la  expresión  C:=  D;  seguirá  siendo  incorrecta,  esto  porque  esta  declaración    múltiple  es  sólo  una  forma abreviada de las dos declaraciones anteriores.

                El que se defina o no un nuevo tipo depende mucho del grado de abstracción que se pretenda lograr.

                Si se está pensando en los arreglos como objetos que serán manipulados (asignados, mezclados, etc..) entre ellos,  se  hace  necesario  definir  un  tipo.  Si,  por  otro  lado,  se  conceptualiza  al  arreglo  sólo  como  una agrupación  de  elementos  indexables,  sin  relación  alguna  con  otros  arreglos  semejantes,  entonces  lo  más recomendable es definirlo como de tipo anónimo.

                El modelo de tipos arreglo presentado no es del todo satisfactorio, pues no nos permite representar una abstracción que agrupe arreglos con diferentes límites, pero que para todos los demás efectos forman una sola familia. En particular, no nos permite escribir subprogramas que puedan tomar arreglos de largo variable como parámetros reales. De ahí que Ada introduce el concepto de tipo arreglo no restringido, en el cual no se expresan los límites al momento de la declaración. Consideremos

                type VECTOR is array (INTEGER range <>) of REAL; (Al símbolo compuesto <> se le denomina “caja”).

                Con  esto  se  está  diciendo  que  VECTOR  es  el  nombre  de  un  tipo  arreglo  unidimensional  de componentes REAL   con índice INTEGER. Sin embargo, los límites del rango no se han especificado. Esta información debe (obligatoriamente) aportarse cuando se declaran variables o constantes de tipo VECTOR. Esto  puede  hacerse  de  dos  formas.  Podemos  introducir  un  subtipo  intermedio  y  luego  declarar  los  objetos (variables y/o constantes).

                subtype VECTOR_5 is VECTOR(1..5); V: VECTOR_5;

                o podemos declarar los objetos directamente

                V:VECTOR(1 .. 5);

                En ambos casos los límites deben darse mediante una restricción de índices que toman la forma de

                un rango discreto entre paréntesis.

                El  índice  del  tipo  arreglo  no  restringido  también  puede darse a través de un subtipo. Entonces, si

                tenemos

                type P is array (POSITIVE range <>) of REAL;

                los límites reales de cualquier objeto deben estar dentro del rango estipulado por el subtipo POSITIVE.

                Otra definición de tipo muy útil es

                type MATRIX is array (INTEGER range <>, INTEGER range <>) of REAL;

                la que luego nos permite definir subtipos como

                subtype MATRIX_3 is MATRIX(1 ..3, 1 .. 3);

                y también variables

                M:MATRIX(1 .. 3, 1 .. 3);

                Volvamos al asunto de la asignación entre arreglos completos. Para realizar este tipo de asignación

                es necesario que los arreglos ubicados a ambos lados del comando de asignación sean del mismo tipo y que

                sus  componentes  puedan  ser  pareados.  Esto  no  significa  que  los  límites  tengan  que  ser  necesariamente iguales, sino sólo que la cantidad de componentes en las dimensiones correspondientes sea la misma. De ahí que podamos escribir

                V:VECTOR(1 .. 5); W:VECTOR(0 .. 4);

                . . . V:= W;

                Tanto V como W son de tipo VECTOR y tienen 5 componentes. También sería válido escribir

                P:MATRIX(0 .. 1, 0 .. 1);

                Q: MATRIX(6 .. 7, N .. N+1);

                . . . P:= Q;

                La igualdad y diferencia entre arreglos siguen reglas similares a la asignación. Dos arreglos pueden

                ser  comparados  sólo  sin  son  del  mismo  tipo,  y  son  iguales  si  sus  dimensiones  correspondientes  tienen  el mismo número de componentes y si los componentes pareados son iguales.

                Los atributos FIRST, LAST, LENGHT y RANGE también pueden aplicarse a tipos y subtipos en  la medida estén restringidos, entonces

                VECTOR_6′LENGHT = 6

                pero

                VECTOR’LENGHT   es ilegal.

                6.3. Caracteres y strings

                Completaremos la discusión de los tipos de enumeración introduciendo los tipo caracter. En los tipos

                de enumeración vistos anteriormente, tal como

                type COLOUR is (RED, AMBER, GREEN);

                los valores habían sido representados por identificadores. También es posible tener un tipo de enumeración en

                el que parte o todos los valores están representados por literales de caracter.

                Un  literal  de  caracter  es  otra  forma  que  puede  tomar  un  lexema.  Consiste  de  un  caracter  único encerrado entre apóstrofes. El caracter debe ser un caracter imprimible o un espacio blanco. No puede ser un caracter de control, tales como tabulador horizontal o nueva línea.

                Este es un caso donde se hace distinción entre mayúsculas y minúsculas. De ahí que los literales

                ‘A’          y              ‘a’

                sean diferentes.

                Basados en lo dicho, podemos definir el tipo de enumeración.

                type ROMAN_DIGIT is (‘I’, ‘V’, ‘X’, ‘L’, ‘C’, ‘D’, ‘M’);

                y luego declarar

                DIG: ROMAN_DIGIT:= ‘D’;

                Además, son aplicables todos los atributos de los tipos de enumeración. ROMAN_DIGIT’FIRST = ‘Y’

                ROMAN_DIGIT’SUCC(‘X’) = ‘L’ ROMAN_DIGIT’POS(‘M’) = 6

                Existe  un  tipo  de  enumeración predefinido de nombre CHARACTER que es (obviamente) el tipo caracter. Su declaración es aproximadamente

                type CHARACTER is (null, . . ., ‘A’, ‘B’, ‘C’, . . ., del);

                Nótese que la introducción de los tipos ROMAN_DIGIT y CHARACTER lleva a la sobrecarga de algunos literales. De ahí que la expresión

                ‘X’  < ‘L’

                sea ambigua. No sabemos si se están comparando caracteres de tipo ROMAN_DIGIT o CHARACTER. Para resolver esta ambigüedad se debe cualificar uno o ambos argumentos

                CHARACTER’(‘X’) < ‘L’ = FALSE ROMAN_DIGIT’(‘X’) < ‘L’ = TRUE

                Así como existe el tipo predefinido CHARACTER, existe el tipo predefinido STRING

                type STRING is array (POSITIVE range <>) of CHARACTER;

                Este es un tipo arreglo normal y por lo tanto sigue las reglas previamente enunciadas. Por lo tanto, podemos escribir

                S:STRING(1 .. 7);

                para declarar un arreglo de rango 1 .. 7. En el caso de arreglos constantes es posible deducir los límites a partir del valor inicial. Entonces, si escribimos

                G: constant STRING:= (‘P’, ‘I’, ‘G’);

                el límite inferior de G (es decir, G’FIRST) será 1, puesto que el tipo del índice de STRING es POSITIVE y

                POSITIVE’FIRST es 1.

                Existe  una  notación  alternativa  más  abreviada  para  los  strings,  esta  consiste  en  una  cadena  de caracteres encerrada entre comilla dobles. El ejemplo anterior quedaría

                G: constant STRING:= “PIG”;

                El  uso  más  importante  de  los  strings  es,  obviamente,  la  creación  de  textos  de  salida.  Es  posible “imprimir” una secuencia simple de caracteres usando el subprograma (sobrecargado) PUT. Por ejemplo, la llamada

                PUT(”The Countess of Lovelace”);

                colocará el texto

                The Countess of Lovelace en algún archivo apropiado.

                Es  posible  aplicar  los  operadores  relacionales  <,  <=,  >,  y  >=  sobre  los  string.  Las  reglas  son  las ampliamente usadas en distintos lenguajes. Por ejemplo, todas las comparaciones siguientes son verdaderas.

                “CAT” < “DOG”

                “CAT” < “CATERPILLAR” “AZZ” < “B”

                “” < “A”

                Existe,  además,  el  operador  de  concatenación  &,  que  permite  unir  dos  strings  para  generar  uno nuevo. El límite inferior del resultado es igual al límite inferior del primer operando. Por ejemplo, si tenemos

                STRING_1:STRING:=”CAT”; STRING_2:STRING(11 .. 18):=”ERPILLAR”;

                entonces

                STRING_1 & STRING_2 =  ”CATERPILLAR” STRING_2 & STRING_1 =  ”ERPILLARCAT”

                es decir, dos strings de igual largo, pero con rangos  diferentes, en la primera expresión el rango es 1 .. 11 y la segunda  11  ..  21.  En  todo  caso,  cuando  se  están  generando  strings  para  salidas  los  rangos  en  sí  no  son relevantes.

                Finalmente, es posible tomar sólo una parte de un string, ya sea para utilizarlo en una expresión, o para asignar ciertos valores  a una parte específica de un string. Por ejemplo, si escribimos

                S:STRIG(1 .. 10):= “0123456789″;

                . . .

                T: constant STRING:=S(3 .. 8);

                . . .

                S(1 .. 3):= “ABC”;

                al  final  T  tendrá  el  valor  ”234567″,  con  T’FIRST  =  3  y  T’LAST  =  8.  Y  S  terminará  con  el  contenido

                “ABC3456789″.

                Ejercicio:

                1. Declare un arreglo   constante de nombre ROMAN_TO_INTEGER que pueda ser usado como una tabla para convertir un ROMAN_DIGIT en su entero equivalente. Por ejemplo, para convertir ‘C’ en 100.

                2.  Escriba  código  Ada  que  tome  un  objeto  R  de  tipo  ROMAN_INTEGER  y  lo  calcule  el  valor  entero correspondiente en la variable V. Debe asumirse que R es un número romano correctamente escrito.

                6.4. Registros

                Un  registro  es  un  objeto  compuesto  cuyos  componentes  tienen  nombres  y  pueden  ser  de  tipos distintos. A diferencia de los arreglos, no es posible tener registros de tipo anónimo. Es obligación definir un tipo  registro  explícito  para  posteriormente  crear  los  objetos  (constantes  y/o  variables)  correspondientes. Consideremos el siguiente tipo registro

                type MONTH_NAME is (JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC);

                type DATE is record

                DAY:INTEGER range 1 .. 31;

                MONTH:MONTH_NAME; YEAR:INTEGER;

                end record;

                DATE  es  un  tipo  registro  que  contiene  tres  componentes  con  nombre:  DAY,  MONTH  y  YEAR. Ahora, podemos definir variables y constantes de la manera usual.

                D:DATE;

                declara  una  variable  de  tipos  DATE.  Mediante  la  notación  punto  se  pueden  accesar  los  componentes individuales de D. Así,  para asignar valores a estos componentes podemos escribir

                D.DAY:= 15; D.MONTH:=AUG; D.YEAR:= 1959;

                Los registros pueden ser manipulados como objetos completos. Además, es posible asignar valores a todos los componentes de un registro mediante un conjunto ordenado. Por ejemplo

                D:DATE:= (15,AUG,1959); E:DATE;

                . . . E:= D;

                Es posible dar valores por omisión a todos o algunos de los componentes. Por ejemplo, al escribir

                type COMPLEX is record

                RL: REAL:= 0.0;

                IM: REAL:= 0.0;

                end record;

                se está declarando un tipo que contiene dos componentes de tipo REAL y cada uno de ellos se le da un valor por omisión de  0.0. Ahora podemos definir

                C1:COMPLEX;

                C2: COMPLEX:= (1.0, 0.0);

                La  variable  C1  tiene  sus  componentes  con  los  valores  por  omisión,  es  decir,  0.0.  Por  su  parte,  la variable C2 recibe valores que reemplazan a los indicados en la definición del tipo. Nótese que a pesar que el segundo   componente   de   C2   toma   el   mismo   valor   estipulado   por   omisión,   es   necesario   indicarlo explícitamente. Esto se debe a que cuando se asignan los valores mediante un conjunto, es obligación dar el valor para todos y cada uno de los componentes.

                Las únicas operaciones predefinidas para los registros son = y /=, y la asignación. Es posible realizar otras operaciones a nivel de registro o definirlas explícitamente en subprogramas, como se verá más adelante.

                Los componentes de un registro pueden ser de cualquier tipo; entre otros pueden ser otros registros o arreglos.  Sin  embargo,  si  el  componente  es  un  arreglo,  este  debe  ser  restringido  y  no  puede  ser  de  tipo anónimo. Y, obviamente, un registro no puede contener una instancia de sí mismo.

                Los componentes no pueden ser constantes, pero un registro en sí si puede serlo. Por ejemplo, para representar la unidad imaginaria (raíz cuadrada de -1) podemos declarar

                I: constant COMPLEX:= (0.0, 1.0);

                A continuación presentamos un ejemplo más elaborado de un registro

                type PERSON is

                record

                BIRTH: DATE; NAME:STRING (1 .. 20); end record;

                El registro PERSON tiene dos componentes, el primero es otros registro y el segundo es un arreglo. Ahora podemos escribir, por ejemplo

                JOHN:PERSON; JOHN.BIRTH:=(19, AUG, 1937); JOHN.NAME(1 .. 4):=”JOHN”;

                y tendremos

                JOHN := ((19, AUG, 1937), “JOHN                    “); Ejercicio:

                1. Declare tres variables C1, C2 y C3 de tipo COMPLEX; y escriba uno o más sentencias para realizar la suma y  el producto de C1 y C2, guardando el resultado en C3.

                7. Subprogramas

                En Ada existen dos tipos de subprogramas: funciones y procedimientos. Las funciones retornan un valor y son usadas en expresiones, mientras que los procedimientos no retornan valor y son llamados como instrucciones.

                Las acciones a realizar en   un subprograma se describen dentro de lo que se denomina “cuerpo del subprograma”, el que es declarado de la manera usual en la parte declarativa de, por ejemplo, un bloque u otro subprograma.

                7.1. Funciones

                Todas las funciones comienzan con la palabra reservada function seguida del nombre (identificador)

                de  la  función.  Si  existen  parámetros,   después  del  identificador   se  entrega  una  lista  con  los  parámetros (separados  por  ”;”)  encerrada  entre  paréntesis.  Luego  de  la  lista  de  parámetros  (si  existe)  viene  la  palabra return y el tipo (o subtipo) del valor de retorno de la función. Tanto el tipo de los parámetros como del valor

                de retorno debe ser indicado con un identificador de tipo (o subtipo) declarado previamente. Por ejemplo, no

                es posible indicar que el valor de retorno será un cierto rango de los enteros indicándolo explícitamente luego

                de  la  palabra  return,  sino  que  es  necesario  definir  un  subtipo  para  dicho  rango  con  anterioridad  a  la declaración de la función y escribir el nombre del subtipo a continuación de la palabra return.

                A la parte descrita hasta ahora  se le conoce como “especificación de la función” y es la que entrega

                los datos para el entorno, en el sentido que en ella se entrega la información necesaria y suficiente para llamar

                a (hacer uso de) la función.

                Después de la especificación viene la palabra is seguida del cuerpo de la  función, que es semejante a

                un   bloque:  una  parte  declarativa,  begin,  una  secuencia  de  instrucciones  y  end.  Como  en  el  caso  de  los bloques, la parte declarativa puede no existir, pero al menos debe existir una instrucción.

                En  algunos  casos  (como  veremos  más  adelante)  es  necesario  escribir  sólo  la  especificación  de  la función, sin su cuerpo. En este caso en lugar de la palabra is va un “;”.

                Los parámetros formales son objetos locales de una función y actúan como constantes cuyos valores

                iniciales son calculados de acuerdo a los correspondientes parámetros reales. Cuando una función es llamada (utilizada dentro de un expresión) se elabora la parte declarativa de la manera usual y luego se ejecutan las instrucciones.  Para  entregar  el  valor  de  retorno  se  utiliza  la  instrucción  return,  la  cual  además  entrega  el control al lugar desde donde se hizo la llamada.

                Consideremos nuestro ejemplo de la raíz cuadrada:

                function SQRT(X:REAL) return REAL is

                R:REAL;

                begin

                –  calcular valor de la raíz de X y guardarlo en R

                return R;

                end SQRT;

                La función puede ser utilizada, por ejemplo, en

                T:REAL:= 0.3;

                . . .

                S:= SQRT(T + 0.5) + 3.6;

                para ello se evalúa la expresión T + 0.5   (es decir, T + 0.5 es el parámetro real) y se asigna al parámetro formal X, el que dentro de la función se comporta como una constante, con valor inicial 0.8. Esto equivale a tener

                X: constant REAL := T + 0.5;

                Luego  se  elaboran  las  declaraciones,  si  las  hubiera,  y finalmente  se  ejecutan  las  instrucciones.  La última de las cuales es la instrucción return, la que retorna el control a la expresión “SQRT(T + 0.5) + 3.6″ junto con el valor contenido en R.

                La expresión de una instrucción return puede ser de cualquier nivel de complejidad, lo importante

                es que el resultado de dicha expresión sea del tipo indicado como tipo de retorno en la especificación de la función.

                El cuerpo de una función puede contener más de una instrucción return. La ejecución de cualquiera

                de ellas termina la función. Por ejemplo,

                function SIGN(X:INTEGER) return INTEGER is

                Entrega +1, 0 o -1 según sea el signo del entero X

                begin

                if X > 0 then return +1;

                elseif X < 0 then return  -1;

                else

                end if;

                end SIGN;

                return 0;

                Puede  verse  que  la  última  instrucción  no  es  necesariamente  un  return  ,  lo  importante  es  que  la

                semántica  del  cuerpo  de  la  función  considere  una  instrucción  return  para  todos  los  posibles  casos.  Si  no fuese  así  y  se  llegase  ”end  SIGN;”  el  sistema  entregaría  la  excepción  PROGRAM_ERROR  (este  tipo  de excepción se usa para situaciones en las cuales se ha violado la secuencia de control en tiempo de ejecución).

                Notemos  que  cada  llamada  a  una  función  genera  una  nueva  instancia  de  cada  uno  de  los  objetos

                declarados en ella (incluyendo, lógicamente, los parámetros) y éstos desaparecen cuando la función termina. Por este motivo (es decir, la administración dinámica de memoria) es posible llamar recursivamente a una función. Por ejemplo:

                function FACTORIAL(N:POSITIVE) return POSITIVE is begin

                if N = 1 then return 1;

                else      return N * FACTORIAL(N-1);

                end if;

                end FACTORIAL; Si escribimos

                F:= FACTORIAL(4);

                se produce la llamada recursiva a la función FACTORIAL con parámetros reales 4, 3, 2 y 1. A veces se dice que  una  función  es  recursiva  puesto  que   ”se  llama  a  si  misma”.  Sin  embargo,  es  necesario  entender  que cuando  se  llama  a  FACTORIAL(4)  se  genera  una  instancia  de  la  función,  es  decir,  se  reserva  espacio  de memoria para todos los objetos  locales (más otros datos) y se procede a ejecutar las instrucciones. Cuando se llega   a “4*FACTORIAL(4 - 1)”,   se genera otra instancia de la función, es decir,   se toma más espacio de memoria  para  localizar  los  objetos  locales,  por  lo  tanto  en  este  punto  hay  dos  áreas  de  memoria  para almacenar el estado de cada una de las dos llamadas (FACTORIAL(4) y FACTORIAL(3)). Esto se repetirá,

                en este caso, cuatro veces. Al ejecutar el cuerpo de la llamada FACTORIAL(1)   no se genera una   nueva instancia,  sino  que  retorna  el  valor  positivo  1.  En  este  momento,  la  llamada  FACTORIAL(1)  termina,  sus objetos  locales  dejan  de  existir,  con  lo  que  el  espacio  para  ella  reservado  también  desaparece.  Lo  mismo ocurrirá  para  las  llamadas  FACTORIAL(2),  FACTORIAL(3)  y  finalmente  FACTORIAL(4).  Lógicamente,

                no es necesario repetir el código de la función, cada instancia de la función controla el número de instrucción que se está ejecutando.

                No es necesario chequear que el parámetro sea positivo, pues el  parámetro formal N es del subtipo POSITIVE. Por lo tanto, si se intenta FACTORILA(-2) se obtendrá CONSTRAIN_ERROR. Sin embargo, si intentamos   FACTORIAL(1000)   podría   generarse   una   excepción   STORAGE_ERROR   puesto   que   las instancias de mil llamadas a la función estarían presentes en un   cierto momento. Por otro lado, la llamada FACTORIAL(70) podría generar la excepción NUMERIC_ERROR..

                Como ya se indicó un parámetro formal puede ser de cualquier tipo, pero dicho tipo (o subtipo) debe tener un nombre. Por lo tanto, está permitido que los parámetros puedan ser de tipo array no restringido. Por ejemplo, si definimos el tipo VECTOR como

                type VECTOR is array (INTEGER range <>) of REAL;

                podemos escribir la función

                function SUM(A:VECTOR) return REAL is

                RESULT: REAL:= 0.0;

                begin

                for I in A´RANGE loop

                RESULT := RESULT + A(I);

                end loop;

                return RESULT;

                end SUM;

                En  este  caso  los  límites  del  vector  A  serán  tomados  del  arreglo  de  tipo  VECTOR  usado  como parámetro  real.  Recordemos  que  todas  las  variables  y  constantes  del  tipo  VECTOR  deben  tener  límites definidos.

                Entonces podemos escribir

                V:VECTOR(1 .. 4):= (1.0,2.0,3.0,4.0); W:VECTOR(-1 .. 3):= (1.5,2.5,3.5,4.5,5.5); S:REAL:=SUM(V);

                T:REAL;

                . . . T:=SUM(W);

                con lo que S tomará el valor 10 y T el valor 17.5.

                Lógicamente, en Ada una función puede tener parámetros de tipo array restringido, pero debe ser por medio de un nombre de tipo o subtipo. Por ejemplo, sería incorrecto escribir

                function SUM_6(A:VECTOR(1 .. 6)) return REAL

                debería definirse un tipo o subtipo y luego usarlo para indicar la naturaleza del parámetro. Por ejemplo:

                type VECTOR_6_A is array (1 ..  6) of REAL;

                subtype VECTOR_6_B is VECTOR(1..6);

                function SUM_6_A(A:VECTOR_6_A) return REAL;

                function SUM_6_B(A:VECTOR_6_B) return REAL;

                Lógicamente, el elegir entre definir un nuevo tipo o un subtipo dependerá de los requerimientos del problema  a  resolver.  La  diferencia   entre  SUM_6_A  y  SUM_6_B  radica  en  que  la  segunda  podrá  aceptar arreglos de tipo VECTOR_6_B, VECTOR y otros subtipos de éste (en la media que contengan 6 elementos indexados del 1 al 6), en cambio SUM_6_A sólo aceptará arreglos del tipo VECTOR_6_A.

                Consideremos otro ejemplo:

                function INNER(A,B. VECTOR) return REAL is

                RESULT: REAL := 0.0;

                begin

                for I in A´RANGE loop

                RESULT.= RESULT + A(I) * B(I);

                end loop;

                return RESULT;

                end INNER;

                La función INNER calcula el producto interno entre dos vectores A y B.  Tenemos aquí un ejemplo

                de una función con más de un parámetro.

                La función INNER no es un código robusto, puesto que sólo funciona correctamente cuando ambos parámetros A y B tiene los mismos límites y no se controla los casos en que éstos difieren. Por ejemplo:

                T:VECTOR(1 .. 3):= (1.0, 2.0, 3.0); U:VECTOR(1 .. 3):= (2.0, 3.0, 4.0); V:VECTOR(0 .. 2):= (3.0, 4.0, 5.0); W:VECTOR(1 .. 4):= (4.0, 5.0, 6.0, 7.0);

                . . .

                R:=INNER(T,U);   – R=1.0*2.0 + 2.0*3.0 + 3.0*4.0 = 20.0

                R:=INNER(T,V);     — CONSTRAIN_ERROR al intentar accesar  B(3) R:=INNER(T,W);   – R=1.0*4.0 + 2.0*5.0 + 3.0*6.0 = 32.0

                En  el  tercer  caso  se  obtiene  un  valor  (32.0),  pero  es  erróneo  calcular  el  producto  interno  de  dos vectores  de  distinto  largo.  Sería  deseable  que  el  lenguaje  proveyera  un  mecanismo  para  chequear  en  el momento de la llamada que los límites de A y B coincidan, pero lamentablemente no es así. La solución más adecuada  es  verificar  los  límites  al  comienzo  de  la  función  y,  posiblemente,  generar  explícitamente  una excepción CONSTRAIN_ERROR.

                if A´FIRST /= B´FIRST or A´LAST /= B´LAST then raise CONSTRAIN_ERROR;

                end if;

                Hemos visto que un parámetro formal puede ser un arreglo no restringido, pero el valor de retorno de una función también puede ser un arreglo cuyos límites se definen de acuerdo a los del arreglo entregado por

                la  instrucción  return.  Por  ejemplo,  la  siguiente  función  retorna  un  arreglo  con  los  mismos  límites  del parámetro, pero con los elementos en orden inverso.

                function REV(X:VECTOR) return VECTOR is

                R:VECTOR(X´RANGE);

                begin

                for I in X´RANGE loop

                R(I):= X(X´FIRST + X´LAST -I);

                end loop;

                return R;

                end REV; Ejercicio:

                1.   Escriba una función de nombre PAR que indique  si un entero es par (TRUE) o impar (FALSE).

                2.   Reescriba la función FACTORIAL de forma tal que el parámetro pueda ser positivo o cero (use el subtipo

                NATURAL). Recuerde que FACTORIAL(0) = 1.

                3.   Escriba la función OUTER que entrega el producto externo de dos vectores (posiblemente con distintos rangos). El producto externo de los vectores A y B se define como la matriz Cij  = Ai *Bj.

                4.   Escriba la función MAKE_UNIT que toma un valor positivo N y entrega una matriz unitaria real de NxN.

                Recuerde que una matriz unitaria es aquella que contiene unos un su diagonal principal y ceros en todos los demás elementos.

                5.   Escriba  la  función  MCD  (usando  recursión)  que  entrega  el  máximo  común  divisor  de  dos  enteros  no negativos. Use el algoritmo de Euclides.

                mcd(x,y) = mcd(y, x mod y)    si y ¹ 0

                mcd (x,0) = x

                7.2. Operadores

                Anteriormente dijimos que toda función comienza con la palabra reservada function seguida de un identificador, sin embargo también es posible usar como nombre de función un string de caracteres, siempre y cuando sea alguno de los siguientes operandos (entre comillas).

                and or xor
                = < <= > >=
                + - & abs