Fundamentos de programación con Módula-2 (25)

UNIDAD DIDÁCTICA III

 

 

Tema 11
Formaciones

Las estructuras de datos de tipo formación son quizá las más básicas, o al menos las que se introdujeron primero en los lenguajes de programación imperativos. Puede afirmarse que no hay ningún programa real interesante que no use estructuras de esta clase.

En este tema se introducen tanto estas estructuras en sí, como las operaciones típicas para su manpulación y las técnicas particulares que facilitan su programación, incluyendo centinelas y matrices orladas.

11.1 Necesidad de las formaciones

En el Tema 5 se realizó un programa para ordenar 3 datos. El interés de la ordenación de cualquier tipo de dato (números, nombres, fechas, etc.) resulta más evidente cuando la cantidad de datos a ordenar es de cientos o miles de datos, el trabajo de ordenación resulta tedioso y es más adecuado realizarlo utilizando un computador.

Si se quiere realizar un programa de ordenación con las estructuras de datos presentadas hasta este momento sería necesario declarar tantas variables del mismo tipo como datos se tratan de ordenar. En el programa del Tema 5 se necesitaron tres variables, valorUno, valorDos y valorTres. Además el tratamiento de cada variable se debe realizar por separado: dentro del texto del programa se tienen que hacer las correspondientes comparaciones e intercambios entre cada una de las parejas de variables. Por ejemplo, en el mencionado programa se tenía el siguiente fragmento:

(*– Primer Paso: Ordenar 2 primeros datos –*)
IF valorUno > valorDos THEN
   auxiliar := valorUno;
   valorUno := valorDos;
   valorDos := auxiliar
END;

(*– Segundo Paso: Situar el 3º dato –*)
IF valorTres < valorUno THEN
   auxiliar := valorTres;
   valorTres := valorDos;
   valorDos := valorUno;
   valorUno := auxiliar
ELSIF valorTres < valorDos THEN
   auxiliar := valorDos
   valorDos := valorTres;
   valorTres := auxiliar
END;

Evidentemente esta forma de realizar el programa es imposible de generalizar para la ordenación de un número cualquiera de datos. Además, este mismo problema se reproduce en cualquier programa en el que se trate de manejar una cantidad razonable de datos, todos del mismo tipo. Si tenemos en cuenta que uno de los primeros objetivos de los computadores fue precisamente manejar grandes cantidades de información, se comprende fácilmente porqué ya en los primeros lenguajes de programación se disponía de estructuras de datos para resolver estos problemas. Estas estructuras denominadas genéricamente formaciones, y permiten la generalización de la declaración, referencia y manipulación de datos del mismo tipo. En los siguientes apartados se estudian distintas clases de formaciones y las características de cada una.

11.2 Vectores

Como se muestra en la Figura 11.1, un vector está constituido por una serie de valores, todos ellos del mismo tipo, a los que se les da un nombre común que identifica a toda la estructura globalmente. Cada valor concreto dentro de la estructura se distingue por su índice o número de orden que ocupa en la serie.

Figura(11_1)
       Figura 11.1. Estructura Vector

Como se puede observar, esta estructura es análoga al concepto matemático de vector, en el que el vector completo se identifica por un nombre único y cada elemento particular mediante el correspondiente subíndice:

V = (V1, V2, V3, … , Vn-2, Vn-1, Vn )

En cuanto al aspecto de programación se puede establecer un paralelismo entre la estructura de programación en la que se repite la misma acción un número de veces determinado: sentencia FOR, y la estructura de datos vector en la que también se repiten un número de veces determinado el mismo tipo de dato. Como se verá posteriormente, la sentencia FOR es la que mejor se adecúa al manejo de vectores y en general de todo tipo de formaciones.

11.2.1 Declaración de vectores
En Modula-2, una estructura de tipo vector se declara de la siguiente forma:

TipoVector = ARRAY TipoIndice OF TipoElemento

donde TipoVector es el nombre del nuevo tipo de vector que se declara. El TipoIndice es el tipo al que pertenece el índice y puede ser un escalar enumerable, un subrango o cualquier de los tipos predefinidos CHAR o BOOLEAN. Finalmente TipoElemento puede ser cualquier tipo y corresponde al tipo de dato que constituye cada uno de los elementos del vector. A partir de los tipos siguientes:

TYPE Colores = ( Rojo, Azul, Amarillo );
Longitud = [0..Maximo];
Dimension = [1..Ultimo];
DiasSemana = ( Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo );
HorarioDia = SET OF [0..23];
. . . .

Se pueden hacer las declaraciones de vectores siguientes:

. . . .
Intensidad = ARRAY Colores OF REAL;
TipoVector = ARRAY Dimension OF INTEGER;
Ristra = ARRAY Longitud OF CHAR;
HorarioSemana = ARRAY DiasSemana OF HorarioDia;

En muchos casos el tamaño del vector es un parámetro del programa que podría tener que cambiarse al adaptarlo a nuevas necesidades. Si es así, resulta aconsejable que en la declaración del tipo subrango que dimensiona un vector se declare previamente como constante el valor superior, como se ha hecho en el caso de los valores Maximo y Ultimo. Estas constantes podrían haber sido declaradas previamente de la siguiente forma:

CONST           Maximo = 100;
                      Ultimo = 10;

De esta manera, el programa queda parametrizado por dichas constantes. En las modificaciones posteriores, si se quiere adaptar el tamaño del vector sólo es necesario modificar esta constante. Además, como se verá posteriormente, es habitual utilizar el tamaño del vector en las operaciones de recorrido y búsqueda de los vectores, que se pueden entonces programar en función de la misma constante.

Para poder utilizar los tipos declarados es necesario declarar a su vez, posteriormente, las correspondientes variables. Por ejemplo:

VAR  horarioUno, horarioDos : HorarioSemana;
         frase : Ristra;
         vectorUno, vectorDos : TipoVector;

También es posible declarar una variable como vector, sin necesidad de haber declarado previamente el correspondiente tipo, poniendo la descripción del vector como tipo de la variable. En este caso el tipo es anónimo, es decir, no tiene nombre. Por ejemplo:

VAR   miVector : ARRAY [1..10] OF INTEGER;
          miFrase : ARRAY [0..100] OF CHAR;

En muchos casos es preferible realizar la declaración previa del tipo, dado que sólo serán compatibles aquellas variables que hayan sido declaradas como variables del mismo tipo. Por ejemplo la variable miVector no es compatible con las variables vectorUno y vectorDos, aunque estas últimas sí lo son entre ellas. Como se verá posteriormente, la compatibilidad fija las operaciones que se pueden realizar con los vectores. Por otro lado, resulta mucho más fácil de comprender un programa cuando para todas las variables que no son de un tipo predefinido se declara su tipo previamente.

11.2.2 Operaciones con vectores
La operación más sencilla entre vectores es la asignación de un vector a otro. Por ejemplo:

vectorDos := vectorUno;

Esta operación efectúa una copia de todos los elementos del vector VectorUno en el vector VectorDos. Los valores que tuvieran los elementos del vector VectorDos se pierden. Para poder realizar esta operación es necesario que ambos vectores sean de tipos compatibles. Como se indicó anteriormente, esto sólo se consigue cuando ambos son del mismo tipo, o se han declarado conjuntamente. Se produce un error cuando se trata de realizar una asignación entre vectores de distintos tipos como la siguiente:

miVector := vectorUno;

Habitualmente no se opera con todos los elementos de un vector de una manera global. La mayoría de las operaciones interesantes hay que realizarlas en Modula-2 operando con sus elementos uno por uno. La referencia a un elemento concreto de un vector se hace mediante el nombre del vector seguido, entre corchetes, del índice del elemento referenciado.
Por ejemplo:

miVector [3]
frase [23]
horarioUno [Lunes]

Un elemento de un vector puede formar parte de cualquier expresión con constantes, variables u otros elementos de vector. Para estas expresiones se tendrá en cuenta el tipo de los elementos del vector y las reglas de compatibilidad. Por ejemplo:

miVector[3] := 3*vectorUno[3] + 2*vectorDos[3]
frase[23] := "A"
horarioUno[Lunes] := HorarioDia{9..13, 15..17}

Aunque el vector miVector es incompatible con vectorUno y vectorDos, sus elementos son todos del mismo tipo INTEGER y por tanto, la primera de las expresiones anteriores es totalmente correcta.

Como índice para designar un elemento de un vector se puede utilizar una variable o expresión, siempre que sean compatibles con el tipo declarado para el índice de dicho vector. Por ejemplo:

miVector[j] := 3*vectorUno[i] + 2*vectorDos[i+3]
frase[origen+indice] := " "
horarioUno[dia] := horarioUno[Lunes]

para ello es necesario que las variables utilizadas hayan sido declaradas previamente. Por ejemplo:

VAR i, j : Dimension;
        origen, indice : INTEGER;
       dia : DiasSemana;

Las variables Origen e Indice son de tipo INTEGER, que es compatible con el subrango Dimension.

La posibilidad de utilizar variables o expresiones para el cálculo del índice de un vector es fundamental en cualquier programa que utilice vectores, como se verá a lo largo de este tema. Sin embargo, siempre se debe comprobar exhaustivamente que no existe la posibilidad de que por error el índice calculado por la expresión o guardado en la variable se salga fuera del subrango o tipo enumerado declarado para el vector. No obstante, si esto ocurre el resultado será totalmente impredecible dado que no estaremso haciendo referencia a un elemento del vector que no tiene existencia real. En esta situación, y dependiendo del compilador, se producirá un error de ejecución y/o la parada del programa.

Finalmente, otra operación que se puede realizar con los vectores es utilizarlos como argumentos de procedimientos o funciones. Por ejemplo, si tenemos unas declaraciones de procedimientos tales como las siguientes:

PROCEDURE LeerVector( VAR V: TipoVector )
. . . .
PROCEDURE EscribirVector( V: TipoVector )

para utilizar dichos procedimientos se debe emplear como argumento un vector del mismo tipo de la siguiente forma:

LeerVector( vectorUno )
. . . .
EscribirVector( vectorDos )

En el primer caso, el argumento se pasa por referencia y el procedimiento LeerVector utiliza directamente el vector vectorUno, por lo que puede modificar cualquiera de sus elementos.

En el segundo caso, el argumento se pasa por valor y el procedimiento EscribirVector no puede modificar ningún elemento del vectorDos. En este caso el efecto del procedimiento es equivalente a realizar una copia del vector y operar con dicha copia dentro del procedimiento. Hay que tener en cuenta que si el tamaño del vector es muy grande, la operación de copiar cientos o miles de datos cada vez que se llama al procedimiento puede necesitar más tiempo que la ejecución del propio procedimiento. Además, se necesita una cantidad de memoria adicional igual al tamaño del vector para guardar la copia. Debido a estas razones y por motivos de eficiencia, cuando el vector es muy grande se suele declarar como variable global a todos los procedimientos que los utilizan o se pasa siempre por referencia aunque no se necesite modificar ninguno de sus elementos.

11.3 Formaciones anidadas. Matrices

Según se indicó anteriormente, los elementos de un vector pueden ser de cualquier tipo. En los ejemplos anteriores se han utilizado vectores con elementos de tipo conjunto. Igualmente, es posible definir y utilizar vectores cuyos elementos sean a su vez otros vectores anidados dentro del primero, tal como se muestra en la Figura 11.2.

Figura(11_2)
     Figura 11.2. Formación anidada: Matriz

Estas formaciones anidadas constituyen las matrices y responden al concepto matemático del mismo nombre, en el que el grado de anidamientos expresa si la matriz es de 2, 3 o 4 o más dimensiones.

11.3.1 Declaración de matrices
Mediante vectores, la declaración de una matriz de 2 dimensiones se podría realizar de la siguiente forma:

TYPE MatrizColores = ARRAY Dimension OF ARRAY Colores OF REAL

Sin embargo, esto exige repetir las palabras claves ARRAY y OF con cada nuevo anidamiento. En Modula-2, y habitualmente en todos los lenguajes, se puede evitar esta repetición utilizando una forma genérica para la declaración de formaciones, en la que es posible indicar dos o más TipoIndice separados por comas de la siguiente forma:

TipoMatriz =
         ARRAY TipoIndice1, TipoIndice2, … OF TipoElemento

Esta forma de declaración es más simple y fácil de comprender, y por tanto resulta más aconsejable su empleo. La declaración anterior se realizaría de la siguiente forma:

TYPE MatrizColores = ARRAY Dimension, Colores OF REAL

Al igual que con los vectores, para manejar matrices es necesario declarar las correspodientes variables. Esta declaración también puede realizarse directamente sin la declaración previa del tipo. Por ejemplo:

VAR   matrizUno, matrizDos: MatrizColores;
          A, B, C: ARRAY [ 1..3 ], [ 1..3 ] OF INTEGER;

Como en el caso de los vectores, el programa resulta más flexible cuando se realizan todas las declaraciones de los tipos utilizados. Por ejemplo:

CONST   Mayor = 3;
TYPE
   TipoIndice = [ 1 .. Mayor ];
   TipoMatriz = ARRAY TipoIndice, TipoIndice OF INTEGER;
VAR A, B, C: TipoMatriz;

11.3.2 Operaciones con matrices
Con el requisito de compatibilidad de tipos, siempre es posible asignar una matriz completa a otra. Por ejemplo:

matrizDos := matrizUno;

Para referirse a un elemento de una matriz, hay que tener en cuenta cómo ha sido declarada. Si la matriz fue declarada como vector anidado, se tiene que hacer referencia a un elemento del vector principal, que es a su vez un vector, y dentro de él al elemento del vector anidado y así sucesivamente. Por ejemplo:

matrizUno[5] [Azul]

Si la declaración fue como formación genérica, la referencia a los elementos se realiza separando los índices por comas y encerrados todos ellos entre corchetes. En este caso tenemos:

matrizUno[ 5, Azul ]

Dentro de una matriz se puede hacer referencia a todo un vector o matriz como un único elemento. Por ejemplo:

matrizUno[ 5 ]

es un elemento vector, formado por los valores reales para cada uno de los tres colores (Rojo, Azul y Amarillo). Con estos elementos también es posible realizar asignaciones globales de vectores completos, siempre que sean compatibles. Por ejemplo:

matrizUno[ 3 ] := matrizDos[ 6 ];

Para cualquiera de los índices de una matriz se pueden emplear variables o expresiones, siempre que sean compatibles con el tipo declarado para el correspondiente índice. Al igual que en el caso de los vectores se debe comprobar que tales expresiones o variables nunca dan como resultado un valor que esté fuera del subrango o tipo enumerado declarado para los índices. Por ejemplo, si se ha declarado:

VAR   alfa: Dimension;
          color: Colores;
          i, j: TipoIndice;

será posible escribir

A [ i, j ] := 34
. . . .
matrizUno[ alfa, color ] := matrizDos[ alfa+3, color ]

Finalmente, el paso de una matriz como argumento de un procedimiento o función es totalmente semejante al paso de un vector. En este caso los problemas de eficiencia en el paso de matrices por valor pueden ser mayores al aumentar el número de elementos.

Para terminar este apartado se detallan a continuación las reglas sintácticas que definen la declaración de toda clase de formaciones en Modula-2:

Tipo_formación ::=
          ARRAY Tipo_índice { , Tipo_índice } OF Tipo_elemento
Tipo_índice ::= Tipo_simple

Para el uso de elementos, se tiene:

Variable_indexada ::=
          Varaible_tipo_formación [ Lista_de_expresiones ]
Lista_de_expresiones ::=
          Expresión { , Expresión }

11.4 Esquemas típicos de operación

En la utilización habitual de las formaciones se presentan de forma reiterada ciertas operaciones que responden a un mismo esquema. En este apartado se estudian algunos de los esquemas típicos de operación para el manejo de formaciones.

14.1.1 Recorrido
La operación de recorrido consiste en realizar la misma operación con todos y cada uno de los elementos de una formación. Para esta operación se deben emplear estructuras de tipo FOR. Las variables utilizadas como índices de los FOR deben ser de los mismos TipoIndice utilizados en la declaración de la formación y su rango de variación será desde un valor extremo al otro del rango de la misma declaración. Evidentemente, el recorrido se puede hacer desde el extremo inferior al superior o viceversa según interese en cada situación concreta. Por ejemplo, el esquema para un recorrido sobre el vector horarioUno sería el siguiente:

FOR dia := Lunes TO Domingo DO
   (*– Operaciones a realizar con cada elemento –*)
      horarioUno[ Dia ] := ….
END

Si la formación es de tipo matriz, se necesitan tantos FOR anidados como anidamientos tenga la formación. Por ejemplo, para la formación matrizUno empleando un recorrido descendente para el índice de tipo Dimension, se tiene el siguiente esquema:

FOR alfa := Ultimo TO 1 BY -1 DO
   FOR color := Rojo TO Amarillo DO
      (*– Operaciones a realizar con cada elemento –*)
         matrizUno[ alfa, color ] := ….
   END
END

El recorrido es la operación típica que se debe realizar con una formación cuando se quieren inicializar todos sus elementos. Por ejemplo, la inicialización a cero de todos los elementos de la matriz C, se realiza de la siguiente forma:

FOR i := 1 TO Mayor DO
   FOR j := 1 TO Mayor DO
      C [ i, j ] := 0
   END
END

Si se quieren escribir los valores de la matriz C, por filas, se puede utilizar el mismo esquema, en el que al finalizar cada fila se salta a una nueva línea:

FOR i := 1 TO Mayor DO
   FOR j := 1 TO Mayor DO
      WriteInt( C[ i, j ], 8 )
   END;
   WriteLN
END

La multiplicación de dos matrices A y B para dejar el resultado en la matriz C es un ejemplo muy interesante de recorrido. Primeramente, se inicializa cada elemento de la matriz C[ i, j ] a cero utilizando el esquema anterior. A continuación, al mismo elemento inicializado se le van sumando los productos de cada uno de los elementos de la fila i de la matriz A por los correspondientes elementos de la columna j de la matriz B. Este cálculo de los productos para cada elemento se consigue mediante otro recorrido parcial anidado al de inicialización, sobre los elementos de la fila/columna afectados:

FOR i := 1 TO Mayor DO
   FOR j := 1 TO Mayor DO
      C [ i, j ] := 0;
      FOR k := 1 TO Mayor DO
         C[ i, j ] := C[ i, j ] + A[ i, k ] * B[ k, j ]
      END
   END
END

Fundamentos de programación con Módula-2 (24)

Pruebas de
Evaluación a Distancia

 

En este apartado se enuncian los ejercicios que constituyen las prácticas y las pruebas de evaluación a distancia de la segunda unidad didáctica. Todos ellos deben ser realizados en Modula-2 utilizando la metodología explicada. El grado de dificultad de los ejercicios es creciente y conviene realizarlos siguiendo el orden de los enunciados.

Los enunciados de los ejercicios son los siguientes:

1.- Realizar una función que devuelva un valor booleano cierto o falso si el número que se le pasa como argumento es "perfecto". Para que un número sea "perfecto" es necesario que su valor sea igual a la suma de todos sus divisores incluyendo al 1 y sin incluirle a él mismo. Por ejemplo, los divisores de 6 son el 1, el 2  y el 3; su suma es igual a 6, luego el número 6 es "perfecto". Utilizando la función anterior realizar un programa que escriba la lista de números "perfectos" hasta uno dado, introducido como dato al programa.

2.- Realizar un procedimiento que lea una letra, compruebe que es una de las utilizadas para escribir los números romanos y devuelva su valor, según la siguiente tabla:

          I      –         1                               C          –          100
          V    –          5                               D         –           500
          X     –         10                              M         –          1000
          L     –         50                              Resto   –             0

Utilizando el procedimiento anterior, realizar otro procedimiento que lea un número romano y devuelva su valor entero correspondiente. Este procedimiento tendrá en cuenta las reglas de escritura de los números romanos. Finalmente, utilizando este último procedimiento realizar un programa que lea dos números romanos e indique cuál de ellos es mayor.

3.- Realizar un programa que escriba todas las permutaciones posibles que se pueden obtener a partir de las 4 letras: A, B, C y D. Se sugiere construir cada permutación añadiendo elementos de uno en uno a un conjunto. Esto evitará la repetición de un carácter en la misma permutación y permitirá verificar cuando se ha completado una nueva permutación.

4.- Realizar una función que devuelva el día de la semana cuando se le pasan como argumento el día, el mes y el año de una fecha cualquiera. Utilizando la función anterior escribir un programa al que se le introducen como datos el mes y el año y devuelve como resultado la hora del calendario de dicho mes. Por ejemplo:

¿Mes? 5
¿Año? 2000

                     MAYO     2000
LU      MA      MI      JU      VI      SA      DO
  1         2        3        4       5        6         7
  8         9       10      11     12      13        14
15       16      17      18     19       20        21
22       23      24      25     26       27        28
29       30      31

5.- Realizar un programa que analice un texto terminado en el carácter punto (.) y elabore las siguientes estadísticas:

– Número total de palabras del texto
– Número de palabras que utilizan N o más vocales diferentes
– Número de palabras que utilizan M o más consonantes diferentes

Los valores de N y M se leerán como datos del programa.

6.- Realizar un programa para controlar las plazas de un aparcamiento. El aparcamiento dispone de 24 plazas de dos tamaños diferentes: 14 pequeñas y 10 grandes con la disposición que se muestra a continuación:

Ejercicio6

La asignación se realizará automáticamente según el tamaño del vehículo que se quiere aparcar con el siguiente algoritmo:

– Cada vehículo solamente ocupará una plaza.
– Un vehículo pequeño siempre ocupará una plaza pequeña, salvo que estén todas ocupadas y exista alguna grande libre.
– Un vehículo grande sólo puede aparcar en una plaza grande. Si todas están ocupadas no podrá aparcar aunque estén todas las pequeñas libres.
– De todas las plazas libres, siempre se ocupará primero la de número menor.

El programa tendrá 3 opciones básicas:

Entrada: es necesario indicar el tamaño de coche ( P/G ).
Salida: es necesario indicar la plaza que se deja libre. Por ejemplo: P 5
Situación del aparcamiento: indicando las plazas libres y las ocupadas.

Fundamentos de programación con Módula-2 (23)

10.3 Equivalencia entre estructuras

Como ya ha sido explicado, las estructuras básicas estrictamente necesarias para la programación estructurada son la selección (IF – THEN – ELSE – END) y la iteración (WHILE – DO – END). Éstas se pueden considerar las estructuras primarias. El resto, que denominaremos estructuras secundarias, son en general más complejas y tienen como objetivo lograr programas más sencillos en situaciones particulares.

Cualquiera de las estructuras secundarias siempre puede ser expresada en función de las primarias. Sin embargo, no siempre es posible expresar una sentencia primaria en función de una secundaria. Por ejemplo, no se puede realizar una iteración WHILE condicionada por más de una variable de tipo simple mediante una estructura FOR. Tampoco es posible realizar una selección IF condicionada por los valores que toma una expresión real mediante una estructura CASE.

Aunque a lo largo de las explicaciones del Tema 5 y este mismo tema se han ido mostrando ciertas transformaciones y equivalencias entre estructuras, en este apartado a modo de resumen se muestra cómo cualquier estructura secundaria se puede realizar mediante las estructuras primarias.

10.3.1 Selección general
La selección general se consigue mediante selecciones primarias anidadas. Dada la estructura siguiente:

IF          Condición1 THEN SentenciasA
ELSIF    Condición2 THEN SentenciasB
. . . .
ELSIF    CondiciónN THEN SentenciasJ
ELSE                                SentenciasK
END

se puede realizar de la siguiente forma

IF Condición1 THEN
   SentenciasA
ELSE
   IF Condición2 THEN
      SentenciasB
   ELSE
   . . . .
      ELSE
         IF CondiciónN THEN
            SentenciasJ
         ELSE
            SentenciasK
         END
      END
   . . . .
   END
END

que tiene como incoveniente fundamental la confusión introducida por el anidamiento de los IF – THEN – ELSE – END.

10.3.2 Selección por casos
Esta estructura también se puede realizar mediante selecciones primarias anidadas. Dada la estructura siguiente:

CASE Expresión OF
   v1              : SentenciasA |
   v2..v4         : SentenciasB |
   v5, v6, v7    : SentenciasC |
   …
ELSE              SentenciasH
END

se puede realizar de la siguiente forma

Valor := Expresión;
IF Valor = v1 THEN
   SentenciasA
ELSIF (Valor = v2) OR (Valor = v3) OR (Valor = v4) THEN
   SentenciasB
ELSIF (Valor = v5) OR (Valor = v6) OR (Valor = v7) THEN
   SentenciasC

ELSE
   SentenciasH
END

Esta construcción se puede poner a su vez en función de la estructura básica IF – THEN – ELSE – END, en forma anidada.

10.3.3 Bucle con contador
Esta estructura se puede hacer mediante un control explícito del contador del bucle. Sea la estrucutra siguiente:

FOR Variable := Inicial TO Final BY Incremento DO
   Sentencias
END

Cuando el incremento es positivo se puede realizar de la siguiente forma:

Variable := Inicial;
WHILE Variable <= Final DO
   Sentencias;
   INC( Variable, Incremento )
END

cuando el incremento es negativo la comparación se debe hacer por mayor o igual en lugar de por menor o igual.

10.3.4 Repetición
La sentencia REPEAT se puede transformar en WHILE complementando la condición de repetición y ordenando la ejecución incondicional de la primera iteración. La estrucutra:

REPEAT
   Sentencias
UNTIL Condición

se puede convertir en esta otra:

Sentencias;
WHILE NOT Condición DO
   Sentencias
END

O bien, usando una variable auxiliar para almacenar el valor de la condición:

fin := FALSE;
WHILE NOT fin DO
   Sentencias;
   fin := Condición
END

10.3.5 Bucle indefinido
La transformación de esta estructura puede resultar bastante compleja dependiendo del nivel de anidamiento en el que se produzcan las salidas y el número de salidas existentes. Desarrollaremos la equivalencia sólo para el caso más sencillo. Sea la estructura:

LOOP
   SentenciasA;
   IF Condición THEN EXIT END;
   SentenciasB
END

La estructura WHILE equivalente podría ser:

SentenciasA;
WHILE NOT Condición DO
   SentenciasB;
   SentenciasA
END

Otro esquema equivalente, usando REPEAT, es:

REPEAT
   SentenciasA;
   fin := Condición
   IF NOT fin THEN
      SentenciasB
   END
UNTIL fin

En el caso de estructuras anidadas habría que incluir sentencias de selección para eliminar la parte de ejecución de cada bucle después de una salida, lo cual resultaría bastante complejo. Con esto se pone de manifiesto que hay situaciones en que la estructura de bucle indefinido puede resultar realmente apropiada.

10.4 Ejemplos de programas

En este apartado se recogen algunos programas completos que utilizan las estructuras estudiadas. Algunas partes de estos programas ya han sido utilizadas en los ejemplos presentados a lo largo de este tema.

10.4.1 Ejemplo: Imprimir tickets de comedor
Se trata de confeccionar un ticket mensual de comedor con la cantidad a pagar dependiendo de los días laborables de cada mes y descontando las ausencias de cada individuo. El objetivo fundamental es ilustrar el uso de la selección por casos en función del tipo enumerado de los meses del año. La selección del mes se realiza mediante una sentencia repetitiva que controla que el mes esta entre 1 y 12. Asimismo, el programa permite realizar todos los tickets necesarios hasta indicar que no se quiere continuar mediante un bucle repetitivo controlado por una respuesta del tipo SI/NO. El listado completo del programa es el siguiente:

(**************************************************************************************************
*
*   Programa: Comedor
*
*   Descripción:
*      Programa para realizar el ticket de pago de un comedor
*
**************************************************************************************************)
MODULE Comedor;
   FROM InOut IMPORT
      WriteString, WriteInt, WriteLn, ReadInt, Read, Write;
   CONST
      Comida = 300;        (* Precio comida *)
      ParteIVA = 18;        (* Pesetas de IVA = 6% de 300 *)

   TYPE
      TipoMes = (Enero, Febrero, Marzo, Abril, Mayo, Junio, Julio, Agosto,
             Septiembre, Octubre, Noviembre, Diciembre);
      RangoDias = [0..31];

   VAR
      mes : TipoMes; dias, diasPagar : RangoDias;
      total, totalIVA, aux, faltas : INTEGER;
      tecla : CHAR;

BEGIN
   (*– Leer y validar el mes leido –*)
      REPEAT
         WriteString("¿Mes?");
         ReadInt(aux); WriteLn
      UNTIL (aux > 0) AND (aux <= 12);
      mes := VAL(TipoMes, aux-1);             (* Es necesario empezar por 0 a contar (aux-1) *)

   (*– Calcular días según el mes –*)
      CASE mes OF
         Agosto : dias := 0 |
         Enero, Abril, Diciembre : dias := 17 |
         Febrero, Septiembre : dias := 20 |
         Junio, Noviembre : dias := 21 |
         Marzo, Julio : dias := 22 |
         Mayo, Octubre : dias := 23
      END;

   (*– Realizar os tickets a partir de las ausencias –*)
      REPEAT
         WriteLn; WriteString("¿Nº de Ausencias?");
         ReadInt(faltas); WriteLn; WriteLn;
         diasPagar := dias – VAL(RangoDias, faltas);
         total := diasPagar*Comida;
         totalIVA := diasPagar*ParteIVA;
         WriteString("         RECIBO de COMEDOR"); WriteLn;
         WriteString("Comidas   Precio       Total"); WriteLn;
         WriteInt(VAL(INTEGER, diasPagar), 4);
         WriteInt(Comida, 11);
         WriteInt(total, 13); WriteLn;
         WriteString("               6% IVA       ");
         WriteInt(totalIVA, 6); WriteLn;
         WriteString("            Total Recibo");
         WriteInt(total + totalIVA, 6);
         WriteString( " Pts"); WriteLn; WriteLn;
         WriteString("¿Otro Recibo (S/N)?");
         Read(tecla); Write(tecla); WriteLn;
      UNTIL tecla = "N"
END Comedor.

Un ejemplo del resultado de la ejecución del programa es el siguiente:

¿Mes?4

¿Nº de Ausencias?5

         RECIBO de COMEDOR
Comidas   Precio             Total
  12          300                  3600
               6% IVA              216
            Total Recibo         3816 Pts

¿Otro Recibo (S/N)?S

¿Nº de Ausencias?7

         RECIBO de COMEDOR
Comidas   Precio             Total
  10          300                  3000
               6% IVA              180
            Total Recibo         3180 Pts

¿Otro Recibo (S/N)?N

10.4.2 Ejemplo: Gestión de tarjetas de embarque
Este programa realiza la gestión automática de los asientos de un avión e imprime las tarjetas de embarque. Cuando varios pasajeros solicitan juntos las tarjetas de embarque se les asignan asientos de la misma fila. Si o existen asientos de la misma fila se ocupan los huecos libres. Asimismo, si el número de pasajeros excede el número de asientos por fila se ocupan los huecos libres. Los asientos se gestionan como un conjunto al que se incorporan los nuevos asientos asignados después de comprobar que la asignación ha sido posible. El procedimiento BuscarPlazas es el encargado de realizar el algoritmo para situar las plazas solicitadas en la misma fila o en filas distintas. Otro procedimiento está encargado de imprimir las tarjetas de embarque cuando se ha encontrado sitio.

Para comprobar la situación de los asientos libres se dispone de un procedimiento que imprime las plazas libres y las ocupadas. El programa consiste en un menú que utiliza el procedimiento para la búsqueda y el procedimiento para mostrar la situación según elección del operador. Todo el programa está realizado en base a las constantes del aforo, y el tamaño de la fila. Cualquier tipo de avión se puede gestionar cambiando estas dos constantes. El listado completo del programa es el siguiente:

(**************************************************************************************************
*
*   Programa: Mostrador
*
*   Descripción:
*      Este programa realiza las tarjetas de embarque y asigna plazas en la misma
*      fila siempre que es posible
*
**************************************************************************************************)
MODULE Mostrador;
   FROM InOut IMPORT
      WriteString, WriteLn, WriteInt, Read, Write, ReadInt;
   CONST
      Aforo = 60;        (* Capacidad del avión *)
      Fila = 6;        (* Capacidad de la fila *)
      Pasillo = Fila DIV 2;    (* Capacidad de la mitad de la fila *)
   TYPE
      Plazas = [0..Aforo];
      Asientos = SET OF Plazas;
   VAR
      pasaje : Asientos;
      n : Plazas; Numero : INTEGER;
      opcion : CHAR;
      hay : BOOLEAN;

PROCEDURE PintarPlazas( P : Asientos);
(*=======================================
Procedimiento para dibujar la ocupación de plazas
========================================*)
   VAR i : Plazas;
BEGIN
   WriteLn; WriteLn;
   FOR i := 1 TO Aforo DO
      IF i IN P THEN              WriteString(" (*)")
      ELSE                           WriteString(" ( )")
      END;
      IF (i MOD Pasillo) = 0 THEN
         WriteString("    ");    (* Pintar el pasillo *)
      END;
      IF (i MOD Fila) = 0 THEN
         WriteLn        (* Saltar nueva fila *)
      END
   END; WriteLn
END PintarPlazas;

PROCEDURE ImprimirTarjetas(B : Asientos);
(*=======================================
Procedimiento para imprimir todas las tarjetas de
embarque de los asientos indicados en B
========================================*)
   VAR
      i : Plazas;
BEGIN
   FOR i := 1 TO Aforo DO
      IF i IN B THEN
         WriteString("———————————"); WriteLn;
         WriteString("|   TARJETA DE EMBARQUE   |");
         WriteLn;
         WriteString("|  Fila :"); WriteInt(((i-1) DIV Fila) + 1, 3);
         WriteString(" Asiento :");
         WriteInt(((i-1) MOD Fila) + 1,3);
         WriteString("  |"); WriteLn;
         WriteString("——————————–"); WriteLn;
      END
   END
END ImprimirTarjetas;

PROCEDURE BuscarPlazas(N : Plazas; VAR Encontradas : BOOLEAN);
(*======================================================
Procedimiento para buscar N plazas
=======================================================*)
   VAR
      i, j, k : Plazas;
      nuevoPasaje : Asientos;
BEGIN
(*– Primero se buscan asientos en la misma fila –*)
   i := 1;
   REPEAT
      j := i; k := 0; nuevoPasaje := Asientos{};
      REPEAT
         IF NOT (j IN pasaje) THEN
            k := k+1; INCL(nuevoPasaje, j);
         END;
         j := j+1;
      UNTIL (k = N) OR (j = 1 + Fila);
      i := i + Fila;
   UNTIL (k = N) OR (i > Aforo);
(*– Si no hay sitio en la misma fila se busa cualquier asiento
      libre hasta completar las plazas –*)
   IF k <> N THEN
      i := 1; k := 0; nuevoPasaje := Asientos{};
      REPEAT
         IF NOT (i IN pasaje) THEN
            k := k+1; INCL(nuevoPasaje, i);
         END;
         i := i+1
      UNTIL (k = N) OR (i > Aforo)
   END;
(*– Si hay sitio se realizan las tarjetas de embarque
      y se modifica el Pasaje –*)
   IF (k = N) THEN
      ImprimirTarjetas(nuevoPasaje);
      pasaje := pasaje + nuevoPasaje
   END;
(*– Resultado de la busqueda: Encontrado o no sitio –*)
   Encontradas := k = N
END BuscarPlazas;

BEGIN
   pasaje := Asientos{};
   REPEAT
      WriteString("¿Opción (Tarjetas, Pasaje, Fin) ?");
      Read(opcion); Write(opcion); WriteLn;
      CASE opcion OF
       "T" :
            WriteString("¿Número de Plazas ?");
             ReadInt(Numero); WriteLn;
             n := VAL(Plazas, Numero);
             BuscarPlazas(n, hay);
             IF NOT hay THEN
                WriteString("IMPOSIBLE"); WriteLn
             END |
      "P" :
            PintarPlazas(pasaje) |
      ELSE
      END
   UNTIL opcion = "F"
END Mostrador.

A continuación se muestra un fragmento de la ejecución del programa:

¿Opción (Tarjetas, Pasaje, Fin) ?P

( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )

¿Opción (Tarjetas, Pasaje, Fin) ?T
¿Número de Plazas ?4
———————————
|   TARJETA DE EMBARQUE         |
|  Fila :  1 Asiento :  1       |
———————————
———————————
|   TARJETA DE EMBARQUE         |
|  Fila :  1 Asiento :  2       |
———————————
———————————
|   TARJETA DE EMBARQUE         |
|  Fila :  1 Asiento :  3       |
———————————
———————————
|   TARJETA DE EMBARQUE         |
|  Fila :  1 Asiento :  4       |
———————————
¿Opción (Tarjetas, Pasaje, Fin) ?T
¿Número de Plazas ?3
———————————
|   TARJETA DE EMBARQUE         |
|  Fila :  2 Asiento :  1       |
———————————
———————————
|   TARJETA DE EMBARQUE         |
|  Fila :  2 Asiento :  2       |
———————————
———————————
|   TARJETA DE EMBARQUE         |
|  Fila :  2 Asiento :  3       |
———————————
¿Opción (Tarjetas, Pasaje, Fin) ?P

(*) (*) (*)     (*) ( ) ( )
(*) (*) (*)     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )
( ) ( ) ( )     ( ) ( ) ( )

¿Opción (Tarjetas, Pasaje, Fin) ?F

10.4.3 Ejemplo: Calculadora
Este programa simula una pequeña calculadora que realiza las cuatro operaciones básicas (suma, resta, multiplicación y división), tiene 4 posiciones de memoria (A, B, C y D) para guardar resultados intermedios y además indica los errores que se producen al introducir las operaciones a realizar. Se pueden introducir todas las operaciones que se necesiten separadas por punto y coma (;). Cada operación está formada por el primer operando, el correspondiente operador (+, -, *, /) y el segundo operando. Un operando puede ser cualquiera de las 4 posiciones de memoria o un valor numérico. Los valores numéricos se indican mediante una secuencia de dígitos del 0 al 9 sin blancos ni ningún otro carácter entre ellos. El resultado de una operación se puede guardar en una posición de memoria indicándolo a continuación de la operación con el símbolo (->) seguido de la posición (A, B, C o D). Se finaliza el programa con un punto (.). Son operaciones válidas las siguientes:

1232*456 -> A; A+34; 45/567.

El programa detecta errores en los operandos, en los operadores y la falta de alguno de los símbolos necesarios. El objetivo fundamental es mostrar un bucle infinito que analiza las operaciones solicitadas y tiene como salidas las que se producen al detectar los errores. Esto constituye un ejemplo de tratamiento de excepciones utilizando las sentencias LOOP y EXIT.

El procedimiento AnalizarSimbolo es el encargado de convertir la secuencia de dígitos en el valor con el que se opera internamente y de indicar el tipo de símbolo analizado en la variable global s. El procedimiento AnalizarOperando devuelve un resultado correcto cuando el operando analizado lo es y devuelve el valor analizado al argumento que se le pasa por referencia.

El programa es un bucle repetitivo en el que se analizan y ejecutan las operaciones que se introducen hasta el punto (.) final. El listado completo es el siguiente:

(**************************************************************************************************
*
*   Programa: Calculadora
*
*   Descripción:
*      Este programa simula una calculadora con las cuatro operaciones básicas
*      (+, -, * y /) y cuatro posiciones de memoria. Analiza las expresiones e indica
*      los errores cometidos en las mismas
*
**************************************************************************************************)
MODULE Calculadora;
   FROM InOut IMPORT
      WriteString, WriteLn, Read, Write;
   FROM RealInOut IMPORT
      WriteReal;
   TYPE
      Tabla = SET OF CHAR;
      Calculos = (Sumar, Restar, Multiplicar, Dividir);
   VAR
      c, s : CHAR;                            (* Último carácter y último símbolo leido *)
      numero, operUno, operDos, resultado, varA, varB, varC, varD : REAL;    (* 4 memorias *)
      operador : Calculos;                        (* Último operador *)
      valido : BOOLEAN;

PROCEDURE AnalizarSimbolo;
(*======================================
Procedimiento para analizar el siguiente símbolo y dejarlo en s
=======================================*)
BEGIN
   WHILE c = " " DO
      Read(c); Write(c)
   END;
   CASE c OF
      "+", "-", "*", "/", ">", "A", "B", "C", "D" : s := c; Read(c); Write(c) |
      ".", ";" :                                                                          s := c |
      "0".."9" :                s := "N"; numero := 0.0;
                                   REPEAT
                                      numero := 10.0*numero + FLOAT(ORD("c") – ORD("0"));
                                      Read(c); Write(c)
                                   UNTIL                   NOT (c IN Tabla{"0".."9"}) |
   ELSE
      s := "E"
   END
END AnalizarSimbolo;

PROCEDURE AnalizarOperando(VAR Oper: REAL; VAR Correcto: BOOLEAN);
(*==========================================================
Función para analizar y obtener un operando correcto
===========================================================*)
BEGIN
   AnalizarSimbolo; Correcto := TRUE;
   CASE s OF
      "N" : Oper := numero |
      "A" : Oper := varA |
      "B" : Oper := varB |
      "C" : Oper := varC |
      "D" : Oper := varD |
      ELSE
         WriteLn;
         WriteString("Error 1: Operando(Número o Variable)");
         Correcto := FALSE
   END
END AnalizarOperando;

BEGIN
   REPEAT
      WriteLn; WriteString("¿Cálculo ?"); WriteLn;
      LOOP
      (*– Analizar operando –*)
         Read(c); Write(c);
         AnalizarOperando(operUno, valido);
         IF NOT valido THEN EXIT END;
      (*– Analizar operador –*)
         AnalizarSimbolo;
         CASE s OF
            "+" : operador := Sumar |
            "-" : operador := Restar |
            "*" : operador := Multiplicar |
            "/" : operador := Dividir
            ELSE
               WriteLn;
               WriteString("Error 2: Operador (+, -, *, /)");
               EXIT
         END;
      (*– Analizar operando –*)
         AnalizarOperando(operDos, valido);
         IF NOT valido THEN EXIT END;
      (*– Ejecutar operación –*)
         CASE operador OF
            Sumar: resultado := operUno + operDos |
            Restar: resultado := operUno – operDos |
            Multiplicar: resultado := operUno * operDos |
            Dividir: resultado := operUno / operDos
         END;
      (*– Analizar asignación a una variable de memoria
           cuando se utiliza el operador -> –*)
         AnalizarSimbolo;
         IF s = "-" THEN
            AnalizarSimbolo;
            IF s <> ">" THEN
               WriteLn;
               WriteString("Error 3: Falta símbolo >");
               EXIT
            ELSE
               AnalizarSimbolo;
               CASE s OF
                  "A" : varA := resultado |
                  "B" : varB := resultado |
                  "C" : varC := resultado |
                  "D" : varC := resultado
                  ELSE
                     WriteLn;
                     WriteString("Error 4: Variables(A, B, C o D)");
                     EXIT
               END;
               AnalizarSimbolo
            END
         END;
      (*– Analizar el punto final o el punto y coma de continuar –*)
         CASE s OF
            ";" : WriteString(" || "); WriteReal(resultado,5); WriteLn |
            "." : WriteString(" || "); WriteReal(resultado,5); WriteLn; EXIT
         END
      END        (* Fin del bucle infinito o el LOOP indefinido *)
   UNTIL s = "."
END Calculadora.

Un ejemplo de ejecución es el siguiente:

¿Cálculo ?
23+56->A; ||  7.900E+1
A*5 -> B; ||  3.950E+2
H
Error 1: Operando (Número o Variable)
¿Cálculo ?
B=
Error 2: Operador (+, -, *, /)
¿Cálculo ?
12+P
Error 1: Operando (Número o Variable)
¿Cálculo ?
12+450M
Error 5: Falta punto y coma
¿Cálculo ?
A+B; ||  4.740E+2
A-B. ||  -3.160E+2

Fundamentos de programación con Módula-2 (22)

Tema 10

Ampliación de Estructuras de Control

Antes de pasar al estudio de las estructuras de datos complejas, se completa el repertorio de estructuras de control más frecuentes en los lenguajes imperativos, detallando las construcciones adoptados en Modula-2.

Se presentan algunas variantes para realizar la iteración y la selección, derivadas de las estructuras fundamentales de la Unidad Didáctica anterior, mostrando la posibilidad que existe de programarlas en función de ellas.

10.1 Estructuras complementarias de iteración

Como se explicó en el Tema 5, la programación estructurada propone la utilización del esquema WHILE como sentencia iterativa fundamental. Esta sentencia basta para poder realizar cualquier programa. Sin embargo, en el mismo Tema 5 también fue introducida la sentencia FOR cuya utilidad fundamental es programar un número de iteraciones que se conoce a priori y que no depende de la evolución de los cálculos realizados en cada iteración. La razón de que exista la sentencia FOR prácticamente en todos los lenguajes, es precisamente lo habitual de este tipo de iteraciones y la facilidad que proporciona el uso de la sentencia FOR en estos casos.

Las estructuras iterativas que se explican en este apartado están también disponibles habitualmente en todos los lenguajes y pretenden facilitar la programación de situaciones concretas pero muy frecuentes en cualquier programa. Utilizaremos para las explicaciones las sentencias disponibles en Modula-2: REPEAT y LOOP. Para posibilitar la terminación de la iteración utilizando una sentencia LOOP es necesario utilizar otra sentencia: EXIT. Esta última sentencia será explicada junto a la sentencia LOOP dado que está ligada por completo a ella y no se puede utilizar nunca de forma aislada.

10.1.1 Sentencia REPEAT
A veces resulta más adecuado comprobar la condición que controla las iteraciones al finalizar cada una de ellas, en lugar de hacerlo al comienzo de las mismas. En este caso, como se muestra en la Figura 10.1, siempre se ejecuta al menos una primera iteración.

Figura(10_1)
     Figura 10.1. Esquema de repetición

El formato de esta sentencia en Modula-2 es el siguiente:

REPEAT Secuencia_de_sentencias UNTIL Condición

la Condición que controla las repeticiones es una expresión cuyo resultado es un valor de tipo BOOLEAN. Si el resultado es FALSE se vuelve a ejecutar la Secuencia_de_sentencias y cuando el resultado es TRUE finaliza la ejecución de la sentencia.

Una situación típica en que resulta cómodo el empleo de esta sentencia es la que se produce cuando al finalizar cada iteración se pregunta al operador si desea continuar con una nueva. En todos estos casos, el programa siempre ejecuta la primera iteración y pregunta si se desea o no realizar otra más. Por ejemplo:

REPEAT
. . . .
… << operación >> …
. . . .
   WriteString( "¿Otra operación (S/N)?" );
   Read( tecla );
UNTIL tecla = "N"

En todos los programas realizados en los Temas anteriores se ha podido programar esta forma de operar utilizando la sentencia WHILE. Sin embargo, en los esquemas desarrollados ha sido necesario forzar la primera iteración inicializando la variable que controla la iteración a un valor distinto del necesario para la finalización. Para el mismo ejemplo anterior, esto se realizaría de la siguiente forma:

tecla := "S";
WHILE tecla <> "N" DO
   . . . .
   … << operación >> …
   . . . .
   WriteString( "¿Otra operación (S/N)?" );
   Read(tecla)
END;

Esta solución es menos elegante y además tiene como incoveniente la necesidad de inicializar la variable de control, con lo que en caso de olvido, la ejecución puede ser impredecible. Como se sabe, las variables pueden tomar aleatoriamente cualquier valor inicial y dependiendo del mismo, se ejecutará o no la primera iteración. Para estas situaciones es muy aconsejable la utilización de la sentencia REPEAT que evita todos estos problemas.

También resulta adecuado el empleo del esquema REPEAT cuando solamente son válidos unos valores concretos para una determinada respuesta. Si la respuesta no es correcta se solicitará de nuevo y no se continuará hasta obtener una respuesta dentro de los valores válidos. La filosofía en este caso es prácticamente la misma del caso anterior, por ejemplo:

REPEAT
   WriteString("¿Mes actual?");
   ReadInt(mes)
UNTIL (mes >0) AND (mes <= 12);

Evidentemente la utilidad de la sentencia REPEAT no está ligada exclusivamente a los casos indicados. En general, es aconsejable su uso cuando se sepa que al menos es necesaria una iteración y por tanto utilizando la sentencia WHILE es necesario forzar las condiciones para que dicha iteración se produzca.

10.1.2 Sentencias LOOP y EXIT
Todas las estructuras iterativas explicadas hasta ahora se construyen mediante una única sentencia del lenguaje. Esto contribuye a que los programas resultantes sean fáciles de entender. La estructura LOOP que se explica en este apartado, sin embargo, emplea dos sentencias independientes para programar un esquema iterativo, que puede ser más complejo y, si no se programa con cierta disciplina, también más confuso. Por tanto, convendrá reservar el empleo de esta construcción para los casos que resulte artificioso emplear alguna de las otras sentencias iterativas explicadas anteriormente.

Figura(10_2)
   Figura 10.2. Bucle indefinido con salida intermedia

Con las sentencias WHILE y REPEAT se puede evaluar la condición para acabar el bucle de iteraciones bien al comienzo o ben al final de la ejecución de la secuencia de sentencias. Con frecuencia los bucles se pueden expresar de una de estas dos formas de manera sencilla. Sin embargo, hay ocasiones en que la condición para terminar las iteraciones se ha de examinar inevitablemente en un punto intermedio distinto al comienzo o final de la iteración. En este caso se tiene la situación que se muestra en la Figura 10.2.

Este tipo de condiciones están ligadas a causas que impiden una continuación normal de la iteración en curso y aconsejan abandonar dicha iteración. Por ejemplo cuando se obtiene un resultado parcial sin sentido: peso, volumen, etc. de valor negativo, que invalida los posibles cálculos posteriores. Además, dentro de este tipo de bucle de iteración suelen existir varias condiciones distintas en distintos puntos por las que se debe abandonar la ejecución del bucle.

Para facilitar la programación de esta estructura se dispone de una sentencia para programar un bucle indefinido sin expresar la condición de terminación, y otra sentencia distinta para poder salir de dicho bucle en el momento deseado. La sentencia de Modula-2 para el bucle indefinido es la siguiente:

LOOP Secuencia_de_sentencias END

que indica que se ejecute siempre de forma repetitiva e incondicional las sentencias agrupadas entre las palabras clave LOOP y END. Para poder finalizar el bucle indefinido es necesario que dentro del mismo exista alguna sentencia de salida. Esta sentencia en Modula-2 contiene simplemente la palabra clave:

EXIT

y su ejecución provoca la salida inmediata desde el interior del bucle indefinido del que ella misma forma parte. La ejecución del programa continúa con la sentencia inmediatamente a continuación del END de dicho bucle.

La sentencia EXIT es incondicional, es decir, no contiene en sí misma el examen de ninguna condición. En un esquema de iteración normal esta sentencia deberá estar anidada dentro de un esquema condicional. En caso contrario, sólo se ejecutaría la parte de la primera iteración anterior a la sentencia EXIT, y al llegar a ella se terminaría inmediatamente el bucle. Un esquema aproximado de la combinación LOOP – EXIT se muestra en la Figura 10.3.

Figura(10_3)
     Figura 10.3. Sentencias LOOP y EXIT

Una sentencia EXIT sólo se puede usar dentro de otra tipo LOOP y se produce un error de compilación cuando se trata de usar fuera de un LOOP.

Un ejemplo típico de esta estructura de iteración es:

LOOP
   WriteString( "¿Mes Actual?");
   RedInt(mes);
   IF (mes > 0) AND (mes <= 12) THEN EXIT END;
   WriteString( "Dato fuera de rango; repita" );
END

En este ejemplo se ha reescrito el bucle para leer como dato el número de un mes, hasta que se introduzca un dato correcto, y añadiendo la escritura de un mensaje de advertencia en el caso de que el dato introducido no sea aceptable.

Dado que existe la posibilidad de anidar sentencias, la sentencia EXIT puede estar anidada dentro de otra u otras sentencias iterativas WHILE, REPEAT o FOR anidadas a su vez dentro del bucle indefinido. Por ejemplo:

LOOP
   ……
   FOR i := 1 TO 10 DO
      ……
      WHILE x > 23 DO
         ……
         IF z = 15 THEN EXIT END;
         ……
      END; (* Final del WHILE *)
      …..
   END; (* Final del FOR *)
   …..
END; (* Final del LOOP *)

Independientemente de la forma y el número de anidamientos, la ejecución de la sentencia EXIT provoca la salida y finalización inmediata del bucle indefinido que lo contenga, sin tener en cuenta para nada las otras sentencias. En el ejemplo anterior, cuando el valor de la variable z es igual a 15 al ejecutar la sentencia IF, se acaban todas las iteraciones incluidas dentro del bucle indefinido, aunque la variable x sea mayor de 23 y el índice de i no haya alcanzado todavía el valor 10.

Cuando se anidan dos o mas sentencias LOOP, cada sentencia EXIT se refiere siempre al bucle más interno de todos aquellos en los que pueda estar anidada. Por ejemplo:

LOOP                           (* Nivel 1º *)
   …..
   … EXIT …                   (* Salida del nivel 1º *)
   LOOP                        (* Nivel 2º *)
      …..
      LOOP                      (* Nivel 3º *)
         … EXIT …              (* Salida del nivel 3º *)
         ……
      END;                       (* Nivel 3º *)
      ……
   … EXIT …                    (* Salida del nivel 2º *)
   END                            (* Nivel 2º *)
   ……
END                               (* Nivel 1º *)

La posibilidad de emplear un número indeterminado de sentencias de salida y el efecto tan drástico que produce cada una de ellas, hace que resulte mucho más difícil de entender un programa en que se abuse de este tipo de estructura iterativa. En cualquier caso conviene evitar el uso de varios bucles indefinidos anidados.

Sólo es aconsejable utilizar esta estructura cuando resulte realmente apropiada (caso de manejo de errores o similar) o siempre que la utilización de las otras estructuras den lugar a un programa artificioso que pueda ser simplificado mediante el empleo de un bucle indefinido y sus correspondientes salidas.

En el apartado de programas completos se muestra un ejemplo para el manejo de errores sintácticos donde resulte más sencillo emplear un bucle indefinido que utilizar otro tipo de sentencia de iteración. El manejo de errores es un caso típico de tratamiento de excepciones según se vio en el apartado 8.3.2 del Tema 8. Las sentencias LOOP y EXIT constituyen un esquema muy adecuado para la programación del tratamiento de excepciones, cuando no interesa utilizar un procedimiento o función con múltiples sentencias de retorno.

10.2 Estructuras complementarias de selección

Para la selección entre varias alternativas es suficiente disponr de la sentencia IF, estudiada en el Tema 5. De hecho, existen lenguajes en los que la única sentencia disponible para la selección es la propuesta por la programación estructurada, que permite solamente la selección entre dos alternativas. La falta de claridad cuando se utilizan varias selecciones anidadas aconseja disponer de una sentencia de selección general como la estudiada en el Tema 5 para el lenguaje Modula-2.

Por las mismas razones de claridad y sencillez es habitual disponer de una sentencia que permite una selección por casos. Esta sentencia se conoce en la mayoría de los lenguajes con la palabra clave que la introduce: CASE. Este apartado está dedicado exclusivamente a dicha sentencia, estudiando la sintaxis y semántica que tiene en Modula-2.

10.2.1 Sentencia CASE
Cuando la selección entre varios casos alternativos depende del valor que toma una determinada variable o del resultado final de una expresión, es necesario realizar comparaciones de esa misma variable o expresión con todos los valores que pueda tomar, uno por uno, para decidir el camino a elegir. Así, en el programa para el cálculo del día de la semana del tema anterior con la variable M que guarda el mes teníamos:

IF         M = Enero        THEN       IncreDias := 0
ELSIF   M = Febrero      THEN      IncreDias := 3
ELSIF   M = Marzo        THEN      IncreDias := 3
ELSIF   M = Abril           THEN      IncreDias := 6
……
ELSIF   M = Octubre      THEN      IncreDias := 0
ELSIF   M = Noviembre   THEN     IncreDias := 3
ELSE    IncreDias := 5
END;

lo que supone una sentencia larga y reiterativa, si se tiene en cuenta que además para algunos casos, por ejemplo los meses de Febrero, Marzo y Noviembre, se tiene que ejecutar la misma acción.

Figura(10_4)
      Figura 10.4. Selección por Casos

Si lo que se necesita es comparar el resultado de una expresión, es la misma expresión la que se debe evaluar tantas veces como comparaciones se deben realizar. En este caso y por razones de simplicidad y eficiencia es aconsejable guardar el resultado de la expresión en una variable auxiliar y realizar las comparaciones con la variable.

Si el tipo de valor que determina la selección es un tipo ordinal: INTEGER, CARDINAL, CHAR, enumerao o subrango, se dispone en Modula-2 de la sentencia CASE cuya estructura se muestra en la Figura 10.4, y en la que se agrupan los casos que tienen el mismo tratamiento y se evalúa solamente una vez la expresión x.

Las distintas vías de ejecución están asociadas a grupos de valores que pueda tomar la expresión o variable x: A con el valor v1, B con los valores de v2 a v4, C con los valores v5, v6 y v7 y establecieno como vía alternativa la acción H para el resto de los valores distintos de los anteriores.

El esquema de esta sentencia es:

CASE valor OF
   valores : acción |
   valores : acción |
   . . . .
ELSE
   acción por defecto
END

La sentencia comienza con la palabra clave CASE y a continuación se indica la expresión o variable cuyo valor fija los casos que se quieren analizar, seguida de la palabra clave OF. Para cada vía de ejecución posible se detallan primeramente los valores que debe tomar la variable, separados por comas (,). Estos valores también se pueden expresar en forma de subrango separados por dos puntos seguidos (..). La secuencia de sentencias que se deben ejecutar se detallan a continuación de los valores y separadas de estos por dos puntos (:). Las distintas vías alternativas de ejecución y sus correspondientes valores se separan unos de otros por el símbolo de barra (|). La alternativa para el resto de los valores es opcional, y va precedida de la palabra clave ELSE. La sentencia finaliza con la palabra clave END.

Esta sentencia no se puede utilizar cuando la variable o el resultado de la expresión que controla la selección sea del tipo REAL u otro tipo no simple como los conjuntos estudiados en el tema anterior. En estos casos no queda más remedio que emplear la sentencia de selección general.

Como ejemplo se reescribe la selección anterior, según el mes:

CASE M OF
   Enero, Octubre :                      IncreDias := 0 |
   Mayo :                                    IncreDias := 1 |
   Agosto :                                  IncreDias := 2 |
   Febrero .. Marzo, Noviembre :   IncreDias := 3 |
   Junio :                                    IncreDias := 4 |
   Septiembre, Diciembre :           IncreDias := 5 |
ELSE
                                                 IncreDias := 6
END;

Evidentemente esta sentencia da lugar a un fragmento de programa más corto y fácil de entender.

En la sentencia CASE se deben incluir todos los posibles valores que pueda tomar la variable o expresión. Cuando se obtiene un valor que no está asociado a ninguna vía (y no hay alternativa ELSE), el programa finaliza por error. Si lo que sucede es que existen valores para los que no se debe realizar ninguna acción, entonces estos valores se deben declarar asociados a una secuencia de sentencias vacía. Por ejemplo:

CASE M OF
   Enero..Mayo:   |
   Julio..Noviembre:   |
   Junio, Diciembre: Sueldo := Sueldo + Extra
END;

otra forma de conseguir esto mismo es mediante una alternativa ELSE vacía. Por ejemplo:

CASE M OF
   Junio, Diciembre: Sueldo := Sueldo + Extra
ELSE
END;

La diferencia entre ambas es que en el primer caso se produciría un error que finalizaría la ejecución del programa si por cualquier causa la variable M toma un valor fuera del rango de mes declarado de Enero..Diciembre. En el segundo caso no se distingue la situación errónea de la que no lo es.

Si el programa está completamente probado, es muy probable que tal situación no se produzca nunca. Sin embargo, cuando un programa está todavía en fase de prueba es importante conocer todos los errores para analizar sus causas y corregirlos.

Por otro lado hay que tener en cuenta que un mismo valor nunca puede estar asociado a distintas alternativas dado que la ejecución resultaría ambigua. Este error puede producirse por una confusión, sobre todo cuando el mismo valor en un caso se indica dentro de un subrango y en otro de forma explícita. Por ejemplo:

CASE M OF
   Enero, Octubre:                               IncreDias := 0 |
   Mayo:                                             IncreDias := 1 |
   Agosto:                                           IncreDias := 2 |
   Febrero..Marzo, Noviembre               IncreDias := 3 |
   Junio:                                              IncreDias := 4 |
   Septiembre..Diciembre:                    IncreDias := 5 |
ELSE
                                                         IncreDias := 6
END;

Esta sentencia resulta ambigua para los meses de Octubre y Noviembre y es, por tanto, errónea.

Para acabar este apartado se detalla la sintasix formal de la sentencia CASE de Modula-2:

Sentencia_CASE ::=   CASE Expresión OF
                                  Caso { | Caso }
                                  [ ELSE Secuencia_de_sentencias ]
                                  END
Caso ::= Lista_de_valores : Secuencia_de_sentencias
Lista_de_valores ::= Valores { , Valores }
Valores ::= Expresión_constante [ .. Expresión_constante ]

El tipo del resultado de la Expresión y los Valores de cada caso deben ser compatibles.

Fundamentos de programación con Módula-2 (21)

9.6.3 Operaciones entre conjuntos
Las operaciones entre conjuntos que se pueden realizar en Modula-2 son: unión, intersección, diferencia y diferencia simétrica. Todas estas operaciones se tienen que realizar entre dos conjuntos del mismo tipo, siendo el resultado otro conjunto también del mismo tipo. Los símbolos de operación y sus significados respectivos son los siguientes:

       Operación                                                 Elementos del conjunto resultante
   X + Y Unión                                                    elementos de X, o de Y, o de ambos
   X * Y Intersección                                            elementos comunes a X e Y
   X – Y Diferencia                                               elementos de X que no están en Y
   X / Y Diferencia simétrica                                 elementos de X o de Y, pero no de ambos

Por ejemplo, dados los conjuntos:

X := Existencias{ Pera, Kiwi, Naranja };
Y := Existencias{ Limon, Pera };

los resultados de las distintas operaciones entre ellos son los siguientes:

       Operación                                                 Resultado en el conjunto Z
   Z := X + Y                                                       Existencias{ Pera, Kiwi, Naranja, Limon }
   Z := X * Y                                                        Existencias{ Pera }
   Z := X – Y                                                        Existencias{ Kiwi, Naranja }
   Z := X / Y                                                        Existencias{ Kiwi, Naranja, Limon }

Además de estos operadores, en Modula-2 existen dos procedimientos predefinidos para manejar conjuntos, que fueron ya mencionados en el Tema 7. Estos procedimientos son los siguientes:

EXCL( S, X )             Excluye el elemento X del conjunto S
INCL( S, X )              Incluye el elemento X en el conjunto S

El primer argumento S debe ser una variable de un tipo conjunto. El valor del elemento X puede darse, en general, en forma de expresión, que habrá de ser del tipo referencial. Por ejemplo:

INCL( marcadoHoy, Pera );
…..
EXCL( boleto, 4*5);
…..
INCL( listaLetras, Caracter );

El mismo resultado se puede conseguir con las operaciones de unión y diferencia, utilizando un conjunto formado por el único elemento a incluir o excluir. Esto es:

mercadoHoy := mercadoHoy + Existencias{ Pera };
…..
boleto := boleto – Tabla{ 4*5 };
…..
listaLetras := listaLetras + Letras{ Caracter };

Además de las anteriores operaciones entre conjuntos, que producen como resultado otro conjunto, también se pueden realizar entre conjuntos operaciones de realción que dan como resultado un valor BOOLEAN. Los operadores disponibles son los siguientes:

       Operación                                              Condición examinada
   X = Y   Equivalencia                                  X e Y tienen los mismo elementos
   X <> Y   Desigualdad                                X e Y tienen algún elemento distinto
   X <= Y   Inclusión                                     todos los elementos de X pertenecen a Y
   X >= Y   Inclusión                                     todos los elementos de Y pertenecen a X

Por ejemplo, a partir de los conjuntos que se indican a continuación:

X := Existencias{ Pera, Kiwi, Naranja };
Y := Existencias{ Limon, Pera };
U := Existencias{ Pera .. Limon };
V := Existencias{ Pera, Kiwi };
W := Existencias{ };

los resultados de distintas operaciones entre ellos son los siguientes:

       Operación                                   Resultado
   Y = U                                              FALSE
   V <= X                                             TRUE
   U <> V                                            TRUE
   X >= W                                            TRUE
   X = Y                                               FALSE
   U >= X                                             FALSE
   U <> Y                                            TRUE
   Y <= W                                           FALSE

Nótese que el conjunto vacío siempre está incluido en cualquier otro.

Otro operador que da lugar a un resultado de tipo BOOLEAN es el operador que permite comprobar si un elemento pertenece o no a un conjunto. Este operador necesita un operando de tipo conjunto y otro del tipo referencial de dicho conjunto, que será el elemento a comprobar. Este operador se indica con la palabra clave IN

Elemento IN Conjunto

El valor del elemento puede darse en forma de expresión. El resultado será TRUE cuando el elemento esté en el conjunto y FALSE en caso contrario. Por ejemplo, para los conjuntos anteriores se tienen los siguientes resultados:

       Operación                                   Resultado
   Pera IN X                                         TRUE
   Limon IN X                                        FALSE

9.7 Ejemplos de programas

Para finalizar este Tema se recogen en este apartado varios ejemplos que utilizan los nuevos tipos introducidos. El primer ejemplo hace especial hincapié en los tipos enumerados y los siguientes están dedicados fundamentalmente al manejo de conjuntos.

9.7.1 Ejemplo: Cálculo del día de la semana de una fecha
Con este programa se calcula qué día de la semana corresponde a una fecha cualquiera que se introduce como dato. Para el cálculo se supone conocido que el día 31 de Diciembre de 1988 fue Sábado. El programa sirve para fechas desde el año 1989 hasta el año 2088 (teniendo en cuenta el EFECTO 2000).

Las declaraciones de los días de la semana, los meses y los subrangos de días y años son las empleadas en los apartados anteriores. Asimismo, se utiliza la función SumarDias descrita en el apartado 9.2.2 para calcular el día de la semana que será dentro de N días conociendo el día de hoy.

La función DiadelaSemana realiza el cálculo del día de la semana teniendo en cuenta los siguientes aspectos:

  • El desfase en días de la semana que se introduce para cada mes, respecto al mismo día del mes de Enero. Por ejemplo, el mes de Febrero son 3, el mes de Marzo 3, .., el mes de Julio 6, etc.
  • Los años bisiestos son los múltiplos de 4.
  • Si el año es inferior a 89 se considera que es posterior al 2000
  • Cada año bisiesto pasado incrementa en 1 día el desfase.

El procedimiento EscribirDia escribe el tipo de día resultante. El listado del programa completo es el siguiente:

(****************************************************************************************
*
*   Programa: Calendario
*
*   Descripción: Programa para el cálculo del día de la semana que
*                      es cualquier fecha desde el 1/I/1989 hasta 31/XII/2088
*
****************************************************************************************)
MODULE Calendario;
   FROM InOut IMPORT
      WriteString, WriteLn, ReadInt, Read, Write;
   CONST
      DiasSemana = 7;
   TYPE
      TipoDia = (Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo);
      TipoMes = (Enero, Febrero, Marzo, Abril, Mayo, Junio, Julio, Agosto, Septiembre, Octubre, Noviembre, Diciembre);
      RangoDias = [0..31];
      RangoAnnos = [0..99];
   VAR
      dia, mes, anno : INTEGER;
      tecla : CHAR;

PROCEDURE DiadelaSemana(D: RangoDias; M: TipoMes; A: RangoAnnos) : TipoDia;
(*==================================================================
Función para calcular el dia de la semana que corresponde a una fecha (D/M/A) cualquiera
===================================================================*)
   CONST
      OrigenA = 89;
      TreintaUnoDiciembre88 = Sabado;
   VAR
      bisiesto : BOOLEAN;
      sumaBisis, sumaAnnos, sumaDias : RangoDias;
   PROCEDURE SumarDias(Hoy : TipoDia; N: RangoDias) : TipoDia;
   (*================================================
   Función para sumar dias de la semana ciclicamente
   =================================================*)
      VAR
         aux : INTEGER;
   BEGIN
      aux := (ORD(Hoy) + N) MOD DiasSemana;
      RETURN VAL(TipoDia, aux);
   END SumarDias;

BEGIN
   IF M = Enero                 THEN sumaDias := 0
   ELSIF M = Febrero        THEN sumaDias := 3
   ELSIF M = Marzo          THEN sumaDias := 3
   ELSIF M = Abril             THEN sumaDias := 6
   ELSIF M = Mayo           THEN sumaDias := 1
   ELSIF M = Junio            THEN sumaDias := 4
   ELSIF M = Julio             THEN sumaDias := 6
   ELSIF M = Agosto         THEN sumaDias := 2
   ELSIF M = Septiembre   THEN sumaDias := 5
   ELSIF M = Octubre        THEN sumaDias := 0
   ELSIF M = Noviembre     THEN sumaDias := 3
   ELSE sumaDias := 5
   END;
   bisiesto := (A MOD 4) = 0;
   IF A < OrigenA THEN
      A := A + 100      (* Año posterior al 2000 *)
   END;
   sumaAnnos := A – OrigenA;  (* Años pasados desde el 89 *)
   sumaBisis := sumaAnnos DIV 4;  (* Bisiestos pasados *)
   sumaDias := sumaDias + D + sumaAnnos + sumaBisis;
   IF bisiesto AND (M > Febrero) THEN
      sumaDias := sumaDias + 1
   END;
   RETURN SumarDias(TreintaUnoDiciembre88, sumaDias)
END DiadelaSemana;

PROCEDURE EscribirDia(S : TipoDia);
(*==================================
Procedimiento para escribir el día de la semana
===================================*)
BEGIN
   IF S = Lunes                 THEN WriteString("Lunes")
   ELSIF S = Martes          THEN WriteString("Martes")
   ELSIF S = Miercoles      THEN WriteString("Miercoles")
   ELSIF S = Jueves           THEN WriteString("Jueves")
   ELSIF S = Viernes         THEN WriteString("Viernes")
   ELSIF S = Sabado          THEN WriteString("Sabado")
   ELSE WriteString("Domingo")
   END;
   WriteLn;
END EscribirDia;

BEGIN
   tecla := "S";
   WHILE tecla <> "N" DO
      WriteString("¿Dia Mes Año?");
      ReadInt(dia); Write(" ");
      ReadInt(mes); Write(" ");
      ReadInt(anno); WriteLn;
      EscribirDia(DiadelaSemana(dia, VAL(TipoMes,mes-1), anno));
      WriteString("¿Otra Fecha (S/N)?");
      Read(tecla); Write(tecla); WriteLn
   END
END Calendario.

Un resultado de la ejecución del programa es el siguiente:

¿Día Mes Año?1 4 93
Jueves
¿Otra Fecha (S/N)?S
¿Día Mes Año?1 10 94
Sabado
¿Otra Fecha (S/N)?N

9.7.2 Ejemplo: Criba de Eratóstenes
En el tema anterior se realizó un programa para imprimir de manera sucesiva los números primos según se calculaban dado que no se podian almacenar en ninguna variable estructurada. Ahora ya disponemos del tipo conjunto y en este ejemplo se trata de obtener la tabla completa de los números primos entre 1 y 500, mediante el método de criba de Eratóstenes, antes de imprimirlos por pantalla. Para ello se parte de un tipo de conjunto Lista definido sobre un referencial subrango de 1 a 500. El procedimiento Eratostenes inicializa el conjunto que se le pasa como argumento con todos los elementos. A continuación se realiza la criba excluyendo aquellos números que son múltiplos de un número primo. La elección del siguiente número primo se efectúa comprobando que no ha sido excluído todavía de la tabla.

El procedimiento EscribirLista escribe los elementos que pertenecen al conjunto de 10 en 10. El listado completo del programa es el siguiente:

(**************************************************************************************************
*
*   Programa: Criba
*
*   Descripción:
*                    Programa para obtener la criba de Eratostenes
*
**************************************************************************************************)
MODULE Criba;
   FROM InOut IMPORT
      WriteString, WriteLn, WriteInt;
   CONST
      Mayor = 500;        (* Valor mayor de la criba *)
   TYPE
      Margen = [1..Mayor];
      Lista = SET OF Margen;
   VAR
      numerosPrimos : Lista;

PROCEDURE Eratostenes(VAR Numeros : Lista);
(*=========================================
Procedimiento para realizar la criba
==========================================*)
   VAR primo, multiplo : CARDINAL;
BEGIN
   Numeros := Lista{1..Mayor};     (* Todos inicialmente *)
   FOR primo := 2 TO Mayor DIV 2 DO
      IF primo IN Numeros THEN
         multiplo := primo + primo;
         WHILE multiplo <= Mayor DO
            EXCL(Numeros,multiplo);
            multiplo := multiplo + primo
         END
      END
   END
END Eratostenes;

PROCEDURE EscribirLista(T : Lista);
(*==========================================
Procedimiento para escribir la lista de 10 en 10 elementos
===========================================*)
   VAR i, j: CARDINAL;
BEGIN
   WriteLn; WriteLn; j := 0;
   FOR i := 1 TO Mayor DO
      IF i IN T THEN
         WriteInt(i,4); j := j + 1;
         IF (j MOD 10) = 0 THEN
            WriteLn    (* Cambiar de línea con 10 elementos *)
         END
      END
   END; WriteLn
END EscribirLista;

BEGIN
   Eratostenes(numerosPrimos);
   WriteString("                 Tabla de Números Primos:");
   EscribirLista(numerosPrimos);
END Criba.

y el resultado de la ejecución es el siguiente:

                Tabla de N·meros Primos:

    1   2     3     5     7  11  13   17   19   23
  29  31   37   41   43  47  53   59   61   67
  71  73   79   83   89  97 101 103 107 109
113 127 131 137 139 149 151 157 163 167
173 179 181 191 193 197 199 211 223 227
229 233 239 241 251 257 263 269 271 277
281 283 293 307 311 313 317 331 337 347
349 353 359 367 373 379 383 389 397 401
409 419 421 431 433 439 443 449 457 461
463 467 479 487 491 499

9.7.3 Ejemplo: Contar letras y dígitos
En este ejemplo se trata de analizar un texto y contar el número de letras, dígitos y espacios en blanco de los que consta. Asimismo, se toma nota de cuáles son las letras y dígitos utilizados. Como resultado se escriben las letras no utilizadas y los dígitos utilizados.

Para almacenar las letras y dígitos utilizados se emplean conjuntos sobre los referenciales desde la A a la Z y desde el 0 al 9. Hay que tener en cuenta que la letra ñ no está incluida en dicho conjunto y por tanto se considera un signo de puntuación. El procedimiento Analizar va incluyendo en cada conjunto los elementos según se encuentran en el texto. El procedimiento DarResultados escribe los resultados obtenidos. El listado completo del programa es el siguiente:

(**************************************************************************************************
*
*   Programa: Contar
*
*   Descripción:
*      Programa para contar las letras y dígitos de un texto
*
**************************************************************************************************)
MODULE Contar;
   FROM InOut IMPORT
      WriteString, WriteLn, WriteInt, Write, Read;
   TYPE
      Caracteres = SET OF CHAR;
      Letras = SET OF ["A".."Z"];
      Digitos = SET OF ["0".."9"];
   VAR
      listaLetras : Letras;
      listaDigitos : Digitos;
      total, totalLetras, totalDigitos, totalBlancos : INTEGER;

PROCEDURE Analizar;
(*===============================
Procedimiento para analizar el texto y contar las letras, dígitos,
blancos y el total de caracteres de un texto
================================*)
   VAR c : CHAR;
BEGIN
   (*– Inicializar –*)
      total := 0; totalLetras := 0; c := " ";
      totalDigitos := 0; totalBlancos := 0;
      listaLetras := Letras{}; listaDigitos := Digitos{};
   (*– Bucle hasta localizar el punto final –*)
      WHILE c <> "." DO
         Read(c); Write(c); INC(total);
         IF c IN Caracteres{"a".."z", "A".."Z"} THEN
         (*– Contar y anotar la letra analizada –*)
            INC(totalLetras); c := CAP(c);
            INCL(listaLetras, c)
         ELSIF c IN Digitos{"0".."9"} THEN
         (*– Contar y anotar el dígito analizado –*)
            INC(totalDigitos); INCL(listaDigitos, c)
         ELSIF c = " " THEN
         (*– Contar el blanco analizado –*)
            INC(totalBlancos)
         ELSE
         END
      END
END Analizar;

PROCEDURE DarResultados;
(*=============================
Procedimiento para escribir los resultados
==============================*)
   VAR c : CHAR;
BEGIN
   WriteLn; WriteLn;
   WriteString("               RESULTADOS"); WriteLn;
   WriteString("               ==========="); WriteLn; WriteLn;
   WriteString("Caracteres Totales:"); WriteInt(total, 4); WriteLn;
   WriteString("Letras Totales:       "); WriteInt(totalLetras, 4); WriteLn;
   WriteString("Dígitos Totales:      "); WriteInt(totalDigitos, 4); WriteLn;
   WriteString("Blancos Totales:    "); WriteInt(totalBlancos, 4); WriteLn; WriteLn;
   WriteString("LISTA DE LETRAS NO UTILIZADAS"); WriteLn;
   FOR c := "A" TO "Z" DO
      IF NOT (c IN listaLetras) THEN Write(c); Write(" ") END
   END;
   WriteLn; WriteLn;
   WriteString("LISTA DE DIGITOS UTILIZADOS"); WriteLn;
   FOR c := "0" TO "9" DO
      IF c IN listaDigitos THEN Write(c); Write(" "); END
   END;
END DarResultados;

BEGIN
   Analizar;
   DarResultados
END Contar.

y el resultado de la ejecución es el siguiente:

En un lugar de la Mancha
a 123 kilometros de Madrid.

               RESULTADOS
               ===========

Caracteres Totales:  52
Letras Totales:         38
DÝgitos Totales:         3
Blancos Totales:       9

LISTA DE LETRAS NO UTILIZADAS
B F J P Q V W X Y Z

LISTA DE DIGITOS UTILIZADOS
1 2 3

9.7.4 Ejemplo: Juego de lotería primitiva
En este último ejemplo se simula el juego de la Lotería Primitiva. El boleto con los números elegidos y la tabla de los números premiados se guardan en un conjunto sobre el referencial de rango 1 a 49. El número complementario se guarda aparta utilizando un tipo subrango.

El procedimiento Sortear genera aleatoriamente los 6 números premiados y el complementario. En todos los lenguajes existe algún sistema para generar números aleatorios. Para generar dichos valores se utiliza el módulo Lib, que contiene un procedimiento RANDOMIZE encargado de iniciar el proceso de generación de números aleatorios y una función RANDOM que devuelve cada vez que se la invoca un nuevo número aleatorio comprendido entre 0 y el valor máximo que se le pasa como argumento (la mayoría de los compiladores de lenguajes de alto nivel incluyen funciones auxiliares o de utilidad, entre las que se encuentra la generación de valores seudoaleatorios). "aquí para descargar el módulo anterior"

En este caso el máximo valor aleatorio es 49. Se generan los 6 números premiados y el complementario por este procedimiento. Con cada nuevo número se comprueba que no figura ya en la Tabla de premiados y que es distinto de 0. En caso afirmativo se incluye en la Tabla.

El procedimiento LeerBoleto funciona de forma parecida al anterior. En este caso se comprueba que los números elegidos están dentro de los posibles (entre 1 y 49). Se permite realizar apuestas múltiples con hasta el doble de números, y se calcula el número de apuestas realizadas con los números elegidos.

El procedimiento EscribirTabla, escribe los números que pertenecen al conjunto que se le pasa como argumento.

Los números acertados se obtienen como el conjunto intersección entre los premiados y el boleto elegido. El complementario se comprueba aparte. El listado completo del programa es el siguiente:

(**************************************************************************************************
*
*   Programa: Primitiva
*
*   Descripción:
*      Programa para jugar a la lotería primitiva con el computador
*
**************************************************************************************************)
MODULE Primitiva;
   FROM InOut IMPORT
      WriteString, WriteLn, WriteInt, ReadInt, Read, Write;
   FROM Lib IMPORT
      RANDOMIZE, RANDOM;
   CONST
      Maximo = 49;        (* Número mayor que se puede elegir *)
      Numeros = 6;        (* Números que forman una apuesta *)
   TYPE
      Rango = [1..Maximo];
      Tabla = SET OF Rango;
   VAR
      boleto, premiados, acertados : Tabla;
      complementario : Rango;

PROCEDURE Sortear(VAR SeisN : Tabla; VAR C : Rango);
(*=============================================
Procedimiento que realiza el sorteo aleatoriamente. Lo devuelve
en una tabla y el número complementario
==============================================*)
   VAR
      i, n : CARDINAL;
BEGIN
   RANDOMIZE;
   SeisN := Tabla{};
   i := 0;
   WHILE i <= Numeros DO
      n := RANDOM(Maximo);     (* Número aleatorio *)
      IF NOT (n IN SeisN) AND (n <> 0) THEN
         IF i < Numeros THEN
            INCL(SeisN,n)
         ELSE
            C := n
         END;
         INC(i)
      END
   END
END Sortear;

PROCEDURE LeerBoleto(VAR B : Tabla);
(*====================================
Procedimiento para leer el boleto en una tabla.
El máximo de números que se pueden seleccionar son 6*2=12
=====================================*)
   VAR
      apuestas, i, aux : INTEGER;
      n : Rango;
      tecla : CHAR;
BEGIN
   B := Tabla{};
   WriteString("Introduzca los números del boleto");
   WriteLn; i := 0; tecla := "S";
   WHILE (i < 2*Numeros) AND (tecla <> "N") DO
      WriteInt(i+1,3); WriteString("º número ?");
      ReadInt(aux); WriteLn; n := VAL(Rango, aux);
      IF NOT (n IN B) AND (n > 0) AND (n <= Maximo) THEN
         B := B + Tabla{n};
         i := i + 1
      END;
      IF ( i >= Numeros) AND (i < 2*Numeros) THEN
         WriteString("¿Más números (S/N)?");
         Read(tecla); Write(tecla); WriteLn
      END
   END;
   IF i = 12 THEN apuestas := 924
   ELSIF i = 11 THEN apuestas := 462
   ELSIF i = 10 THEN apuestas := 210
   ELSIF i = 9 THEN apuestas := 84
   ELSIF i = 8 THEN apuestas := 28
   ELSIF i = 7 THEN apuestas := 7
   ELSE apuestas := 1
   END;
   WriteLn; WriteString("Total apuestas =");
   WriteInt(apuestas, 6); WriteLn; WriteLn
END LeerBoleto;

PROCEDURE EscribirTabla(T : Tabla);
(*=====================================
Procedimiento para escribir los números de una tabla
======================================*)
   VAR
      i : CARDINAL;
BEGIN
   FOR i := 1 TO Maximo DO
      IF i IN T THEN
         WriteInt(i,3)
      END;
   END;
   WriteLn
END EscribirTabla;

BEGIN
   (*– Sortear –*)
      Sortear(premiados, complementario);
   (*– Leer numeros elegidos –*)
      LeerBoleto(boleto);
   (*– Escribir números premiados –*)
      WriteString("Números premiados:");
      EscribirTabla(premiados);
      WriteString("Complementario =");
      WriteInt(complementario, 3); WriteLn; WriteLn;
   (*– Acertados = Coinciden entre boleto y premiados –*)
      acertados := premiados * boleto;
   (*– Escribir aciertos –*)
      WriteString("Números acertados:");
      EscribirTabla(acertados);
      IF complementario IN boleto THEN
         WriteString("y el complementario");
      END
END Primitiva.

Un ejemplo del resultado de la ejecución es el siguiente:

Introduzca los números del boleto
  1º número ?23
  2º número ?12
  3º número ?5
  4º número ?8
  5º número ?37
  6º número ?21
¿Más números (S/N)?S
  7º número ?44
¿Más números (S/N)?N

Total apuestas =     7

Números premiados: 11 21 27 35 37 39
Complementario =  3

Números acertados: 21 37

Fundamentos de programación con Módula-2 (20)

Tema 9
Tipos Definidos:
Enumeración y Conjuntos

 

Después de haber sido introducidas todas las estructuras fundamentales para la construcción de programas, ahora se pasa a estudiar las estructuras de datos. En este Tema se indican las primeras formas en que el programador puede definir sus propios tipos de datos.

En primer lugar se estudian los tipos escalares simples definidos por enumeración y cómo se utilizan. Como caso especial de tipo enumerado ya predefinido se hace especial hincapié en el tipo BOOLEAN, precisando su importancia dentro de la programación. A continuación se estudia la estructura conjunto y se muestra la diferencia que existe entre los conjuntos en Modula-2 y el concepto matemático de conjunto.

Para finalizar, se presentan varios ejemplos que emplean los tipos introducidos y muestran las posibilidades que ofrecen.

9.1 Definición de tipos

Una de las ventajas fundamentales de los lenguajes de alto nivel es la posibilidad que ofrecen al programador de definir sus propios tipos de datos. Los tipos predefinidos: INTEGER, CARDINAL, CHAR y REAL, ya presentados en el Tema 2, nos han permitido la elaboración de programas para la realización de cálculos o el manejo de caracteres. Sin embargo, si se trata de realizar un programa para jugar al ajedrez resulta mucho más adecuado utilizar datos que representen a los peones, caballos, torres, alfiles, reyes y reinas del tablero. Razonamientos similares se pueden hacer si se quieren realizar programas que manejen días de la semana, deportes, colores, alimentos, etc.

Mediante la definición de tipos de datos por el programador se consigue que cada información que maneja el computador tenga su sentido específico. El tipo establece los posibles valores que puede tomar ese dato. Además, al igual que sucedía con los tipos predefinidos, a cada nuevo tipo que se define se asocian un conjunto de operaciones que se pueden realizar con él. Por tanto, la definición de tipos supone crear un nuevo nivel de abstracción dentro del programa.

En Modula-2 la declaración de los tipos se realiza, junto a la declaración de las constantes y variables, dentro de la Parte_declarativa de cualquier Bloque, en el programa principal o en cualquiera de sus procedimientos o funciones. Esta declaración se inicia con la palabra clave TYPE. Por ejemplo:

TYPE
   TipoEdad = INTEGER;
   TipoSexo = CHAR;

Cada tipo se define mediante un nombre o identificador seguido del símbolo igual (=), y a continuación la descripción concreta del tipo que se quiere definir. Esta última parte de la declaración es el objeto de los apartados siguientes de este Tema. Las diferentes definiciones de tipos se separan mediante punto y coma (;).

En las declaraciones anteriores se definen nuevos tipos, haciéndolos equivalentes o sinónimos de otros tipos ya definidos (en este caso, predefinidos). Quizá en estos ejemplos la declaración de tipo no cubre todos los objetivos señalados anteriormente, pues no establece ninguna especificidad. Esto es, convendría establecer que la edad no puede ser negativa ni superior a un valor determinado o que el sexo sólo puede tomar determinados valores. En todo caso es importante señalar que la definición de un nuevo tipo puede utilizar (y normalmente utiliza) otros tipos ya definidos.

La definición de tipos es solamente una declaración de los esquemas de datos que se necesitan para organizar la información de un programa. Para almacenar información es necesario declarar y utilizar variables de los correspondientes tipos, de la misma forma que se hace con los tipos predefinidos.

Por ejemplo, se podrían usar los tipos sinónimos anteriores en la forma:

VAR edad1, edad2: TipoEdad;
        sexo: TipoSexo;
. . . .
edad2 := edad1 + 1;
sexo := "V";
. . . .

De manera formal, la sintaxis y ubicación de la declaración de tipos dentro de la Parte_declarativa del Bloque es la siguiente:

Bloque ::= Parte_declarativa Parte_ejecutiva END
Parte_declarativa ::= { Declaración }
Declaración ::= Declaración_de_constantes |
                       Declaración_de_tipos |
                       Declaración_de_variables |
                       Declaración_subprograma
Declaración_de_tipos ::= TYPE { Definición_de_tipo ; }
Definición_de_tipo ::= Identificador = Esquema_de_tipo
Esquema_de_tipo ::= Identificador_de_tipo |
                                Tipo_enumerado |
                                Tipo_subrango |
                                Tipo_conjunto              (etc…)

En los apartados siguientes se indica la forma en que se pueden definir los esquemas de los tipos enumerados, subrango o conjuntos.

9.2 Tipos enumerados

Aparte de los valores predefinidos básicos (números, caracteres, etc.) en Modula-2 se pueden definir y utilizar nuevos valores simbólicos de la manera que se indica a continuación.

9.2.1 Definición de tipos enumerados
Una manera sencilla de definir un nuevo tipo de dato es enumerar todos los posibles valores que puede tomar. En Modula-2 esta enumeración se realiza mediante una lista con los valores separados por comas (,) y encerrados entre paréntesis. Cada posible valor describe mediante un identificador. Estos indentificadores al mismo tiempo quedan declarados como valores constantes. Por ejemplo:

TYPE
   TipoDia = (Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo);
   TipoMes = (Enero, Febrero, Marzo, Abril, Mayo, Junio, Julio, Agosto, Septiembre, Octubre, Noviembre, Diciembre);
   EstadoCivil = (Casado, Soltero, Viudo, Divorciado);
   Color = (Rojo, Amarillo, Azul);
   Frutas = (Pera, Manzana, Limon, Naranja, Kiwi);
   Cardinal = (Norte, Sur, Este, Oeste);
   Pieza = (Rey, Dama, Alfil, Caballo, Torre, Peon);

La enumeración implica un orden que se establece entre los valores enumerados. Este orden se define de forma implícita e impone que el primer elemento de la lista ocupa la posición 0, el siguiente la 1, y así sucesivamente hasta el último, que ocupa la posición N-1, siendo N el número de elementos enumerados. Los tipos de datos enumerados forman parte de una clase de tipos de Modula-2 denominados tipos ordinales, a la cual pertenecen también los tipos INTEGER, CARDINAL y CHAR, pero no el tipo REAL.

La sintaxis exacta de la declaración de los tipos enumerados es la siguiente:

Tipo_enumerado ::= (Lista_de_identificadores)
Lista_de_identificadores ::= Identificador { , Identificador }

9.2.2 Uso de tipos enumerados
Los tipos enumerados se emplean de manera similar a los tipos predefinidos. El identificador de tipo se puede emplear para definir variables de ese tipo, y los identificadores de los valores enumerados se emplean como las constantes con nombre. Usando las definiciones anteriores podremos escribir:

VAR diaSemana : TipoDia;
        colorCoche : Color;
        mes : TipoMes;
. . . .
diaSemana := Lunes;
colorCoche := Azul;
FOR mes := Junio TO Diciembre DO . . . .
. . . .

Puesto que entre los valores enumerados existe un orden definido, podremos emplear con ellos los operadores de comparación, y escribir:

IF mes >= Julio THEN . . . .
. . . .
WHILE diaSemana < Sabado DO . . . .
. . . .
IF coloCoche = Rojo THEN . . . .

Al igual que para el resto de los tipos ordinales, con los tipos enumerados se pueden utilizar la función predefinida ORD para obtener la posición de un valor en la lista de valores del tipo. Por ejemplo:

ORD( Casado )            = 0
ORD( Kiwi )                 = 4
ORD( Diciembre )         = 11

La operación inversa, que permita conocer qué valor enumerado ocupa una determinada posición, se consigue mediante la función predefinida VAL, que se invoca en la forma:

VAL( T, N )

y devuelve el valor que ocupa la posición N en la colección de valores del tipo T. Con los ejemplos anteriores se cumple que:

VAL( EstadoCivil, 0 )        = Casado
VAL( Frutas, 4 )               = Kiwi
VAL( TipoMes, 11 )          = Diciembre

Otras operaciones aplicables a los tipos ordinales, y por tanto a los enumerados, corresponden a los procedimientos predefinidos INC y DEC, que reemplazan un valor por el siguiente o anterior, respectivamente (o por el N-esimo siguiente o anterior, si se invoca con dos argumentos). Por ejemplo:

colorCoche := Rojo;
mes := Diciembre;
INC( colorCoche );
DEC( mes, 3 )

da como resultado

colorCoche = Amarillo     y      mes = Septiembre

Sin embargo

mes := Diciembre;
INC( mes )

provocará un error, ya que no existe el mes siguiente a Diciembre.

Un dato de tipo enumerado se puede pasar como argumento de procedimientos o funciones y puede ser el resultado de una función. Por ejemplo, si conocemos el día de la semana de hoy y queremos calcular qué día de la semana será dentro de N días, podemos emplear la siguiente función:

PROCEDURE SumarDias( Hoy: TipoDia; N: INTEGER): TipoDia;
   CONST DiasSemana = 7;
   VAR aux: INTEGER;
BEGIN
   aux := ( ORD(Hoy) + N) MOD DiasSemana;
   RETURN VAL( TipoDia, aux );
END SumarDias;

Como se puede observar, primeramente se calcula el ordinal del nuevo dia entre 0 y 6, según el orden establecido en la definición de TipoDia y finalmente se devuelve este ordinal convertido al tipo correspondiente mediante la función predefinida VAL.

9.3 El tipo predefinido BOOLEAN

Al introducir las estructuras de selección o iteración se han descrito sentencias de Modula-2 que utilizan expresiones lógicas o de condición. En ese momento se dijo, de manera informal, que el valor de una condición podía ser cierto o falso. De manera más precisa podemos indicar ahora que en Modula-2 existe el tipo predefinido BOOLEAN que responde a la siguiente definición, análoga a la de un tipo enumerado:

TYPE BOOLEAN = (FALSE, TRUE)

Esta definición no es necesario escribirla ya que está implícita en el lenguaje. El nombre BOOLEAN es el identificador del tipo, y las constantes simbólicas TRUE y FALSE corresponden a los valores de verdad cierto y falso, respectivamente. Como tipo ordinal se cumple:

ORD( FALSE )    = 0
ORD( TRUE )      = 1

A partir del tipo predefinido BOOLEAN, ahora es posible declarar variables de este tipo y utilizarlas, de forma similar al resto de variables, para guardar resultados de expresiones condiconales. Por ejemplo

VAR bisiesto : BOOLEAN;
. . . .
bisiesto := (anno MOD 4) = 0;

Asimismo, es posiblre realizar operaciones entre ellas. En concreto, entre operandos booleanos (variables o no) es posible realizar las operaciones lógicas ya indicadas en el Tema 5 para formar expresiones lógicas y cuyos operandos son los siguientes:

Operador                               Operación lógica
AND o el símbolo &                 Conjunción
OR                                         Disyunción
NOT o el símbolo ~                  Negación

Esto permite formar expresiones y sentencias tales como la siguiente:

IF bisiesto AND (mes > Febrero) THEN
   totalDias := totalDias + 1
END;

Los resultados de las expresiones lógicas para los distintos operandos y operadores son los siguientes:

   a             b           a AND b           a OR b           NOT a
TRUE      TRUE        TRUE              TRUE             TRUE
TRUE      FALSE      FALSE            TRUE             FALSE
FALSE    TRUE

        FALSE            TRUE             TRUE
FALSE    FALSE      FALSE            FALSE           TRUE

El tipo booleano, como cualquier otro tipo enumerado, se puede pasar como argumento de un procedimiento o función y puede ser devuelto como resultado de una función. De hecho es bastante habitual definir funciones cuyo resultado es un valor booleano, cuando se quiere realizar un test sobre los argumentos de la función. Este tipo de funciones se denominan predicados. Un ejemplo de este tipo de funciones es la función predefinida ODD. Esta función determina si el valor del argumento es impar. Por ejemplo:

ODD( 7 )      = TRUE
ODD( 2 )      = FALSE

9.4 Tipos subrango

Otra forma de especificar nuevos tipos de datos es estableciendo rangos parciales de valores de otro tipo ya existente. Con esto no se definen nuevos valores, pero sí un nuevo tipo con una colección limitada de valores válidos.

9.4.1 Definición de tipos subrango
Un tipo subrango se define a partir de otro tipo ordinal ya definido, que se toma como tipo base. La forma de realizar esto es declarar un identificador diferente para el nuevo tipo y establecer los límites mínimo (primero) y máximo (último) del subrango de variación. Estos límites, en Modula-2, se escriben separados por dos puntos seguidos (..) y se encierran entre corchetes:

[primero .. último]

Los valores primero y último serán valores constantes del tipo base que deben cumplir la relación:

primero < último

Por ejemplo:

TYPE Laborable = [ Lunes .. Viernes];
          PrimerSemestre = [ Enero .. Junio];
          Citricos = [ Limon .. Naranja ];
          Porcentaje = [ -100 .. 100 ];
          RangoLetras = [ "A" .. "Z" ];
          RangoDigitos = [ "0" .. "1" ];

Como se puede observar, el tipo base puede ser cualquier tipo ordinal, bien sea predefinido o definido por enumeración. Normalmente no es necesario indicar explícitamente cuál es ese tipo base, ya que se deduce fácilmente de los valores dados como primero y último. Sin embargo hay situaciones ambiguas, y en particular cuando se hacen definiciones como la siguiente:

RandoDias = [ 1 .. 31 ];

La ambigüedad se produce porque este rango de valores enteros positivos es común a los tipos INTEGER y CARDINAL. Por defecto, el compilador consideraría que el tipo base de este ejemplo es CARDINAL. Si queremos que sea INTEGER, podemos indicarlo expresamente en la forma:

RandoDias = INTEGER[ 1 .. 31 ];

Aunque resulte superfluo, la indicación explícita del tipo base se puede hacer siempre que se desee. Las declaraciones anteriores pueden reeescribirse como:

TYPE Laborable = TipoDia[ Lunes .. Viernes];
          PrimerSemestre = TipoMes[ Enero .. Junio];
          Citricos = Frutas[ Limon .. Naranja ];
          Porcentaje = INTEGER[ -100 .. 100 ];
          RangoLetras = CHAR[ "A" .. "Z" ];
          RangoDigitos = CHAR[ "0" .. "1" ];

Sobre el tipo REAL no es posible definir ningún subrango, debido a que este tipo no es tipo ordinal.

Las reglas BNF para la definición de tipos subrango son las siguientes:

Tipo_subrango ::= [ Identificador_de_tipo ]
                           [ Límite_inferior .. Límite_superior ]
Límite_inferior ::= Expresión_constante
Límite_superior ::= Expresión_constante

En esta reglas se indica, además, que para expresar los límites se pueden emplear expresiones cuyo resultado sea un valor constante del tipo adecuado.

9.4.2 Uso de tipos subrango
Las variables de un tipo subrango tienen la misma consideración que las variables de su tipo base. Por tanto, se pueden utilizar en expresiones o sentencias de asignación como si fueran variables del tipo base. Por ejemplo, una variable de tipo RangoLetras se puede utilizar como si fuera de tipo CHAR. La ventaja principal que ofrecen el tipo subrango es que, previamente a cualquier asignación a una variable, se puede comprobar automáticamente que el valor a asignar está dentro de los límites declarados. Si dicho valor está fuera del rango, el programa finaliza inmediatamente por error.

9.5 Tipos estructurados

Todos los tipos de datos presentados hasta este momento se denominan escalares, y son datos simples, en el sentido de que no se pueden descomponer. En general, no tiene sentido tratar de reconocer fragmentos de información independientes dentro de un valor entero, o un carácter, o el valor simbólico de un día de la semana o el número de un día del mes.

En muchas aplicaciones resulta conveniente, o incluso necesario, manejar globalmente elementos de información que agrupan colecciones de datos. Por ejemplo, puede ser apropiado manejar como un dato único el valor de una fecha que incluye la información del día, el mes y el año como elementos componentes separados. Con este objetivo, los lenguajes de programación dan la posibilidad de definir tipos de datos estructurados.

Un tipo estructurados de datos, o estructura de datos, es un tipo cuyos valores se construyen agrupando datos de otros tipos más sencillos. Los elementos de información que integran un valor estructurado se denominan componentes. Todos los tipos estructurados se definen, en último término, a partir de tipos simples combinados.

Los próximos apartados están dedicados al tipo estructurado conjunto, y en temas sucesivos se estudiarán las formaciones y los registros.

9.6 Conjuntos

Con el tipo enumerado Frutas definido en el apartado 9.2.1 sólo es posible tener variables que guarden en cada momento un valor único de fruta. Por ejemplo, si declaramos la variable:

VAR miFruta : Frutas;

con ella sólo es posible guardar un determinado valor de fruta en cada momento:

miFruta := Pera;     (* miFruta ahora es Pera *)
INC(miFruta);          (* miFruta pasaría a ser Manzana *)

Supongamos una frutería que se dedica al comercio de todas las frutas definidas anteriormente por enumeración como el tipo Frutas. Dependiendo de la temporada o del momento del día es posible que alguna de las frutas esté agotada, pero habitualmente la frutería dispondrá de existencias de hasta las cinco clases de frutas diferentes: Pera, Manzana, Limon, Naranja, Kiwi. Si se quiere conocer en cada momento de que fruta hay o no hay existencias sería necesario disponer de tantas variables del tipo Frutas como frutas diferentes hayan sido declaradas. Por ejemplo:

VAR
   fruta1, fruta2, fruta3, fruta4, fruta5 : Frutas;

Los incovenientes de esta solución son los siguientes:

  • Cualquiera de estas variables sería un "contenedor" capaz de guardar cualquier clase de fruta y varias o todas ellas podrían guardar la misma clase de fruta. Hay que recordar que nuestro objetivo sólo es saber de que clase de fruta hay o no hay existencias.
  • Sería necesario poder indicar cuando una variable "contenedor" está vacía y esto no es posible dado que las variables de tipo Frutas sólo pueden contener valores de ese tipo y no existe el valor NoFruta dentro del tipo enumerado Frutas.

Una posible solución alernativa sería declarar tantas variables de tipo BOOLEAN como clases de frutas tenemos. Por ejemplo:

VAR
   hayPera, hayManzana, hayLimon, hayNaranja, hayKiwi: BOOLEAN;

Los incovenientes de esta solución son los siguientes:

  • Estas variables no tienen ninguna relación con el tipo enumerado Frutas y cualquier modificación por separado con el tipo enumerado o en las variables booleanas puede ser causa de un error difícil de detectar y corregir.
  • Para ambas soluciones, con Frutas o BOOLEAN, se necesitan declarar tantas variables como clases de frutas existan y en el caso habitual de disponer de varias decenas de frutas diferentes  está no parecer ser una solución razonable.

Como conclusión se puede decir que necesitamos un nuevo tipo de dato estructurado, capaz de agrupar en una misma variable todos los indicadores de si hay o no hay elementos de un tipo referencial previamente enumerado. Este nuevo tipo de dato estructurado se denomina conjunto y está asociado al concepto matemático de CONJUNTO para el que los matemáticos han establecido un álgebra específica que determina sus propiedades y operaciones.

Como ejemplo, a partir del tipo referencial Frutas podemos definir una variable estructurada de tipo conjunto cuyos posibles valores serían los siguientes:

1 valor con NINGUNA fruta:                           1 valor con TODAS las frutas:

     CONJUNTO VACÍO                                        COJUNTO REFERENCIAL

5 valores con una sola fruta:                          5 valores con cuatro frutas:

     Pera                                                              Pera – Manzana – Limon – Naranja
     Manzana                                                       Pera – Manzana – Limon – Kiwi
     Limon                                                            Pera – Manzana – Naranja – Kiwi
     Naranja                                                          Pera – Limon – Naranja – Kiwi
     Kiwi                                                              Manzana – Limon – Naranja – Kiwi

10 valores con dos frutas:                              10 valores con tres frutas:

     Pera – Manzana                                             Pera – Manzana – Limon
     Pera – Limon                                                 Pera – Manzana – Naranja
     Pera – Naranja                                               Pera – Manzana – Kiwi
     Pera – Kiwi                                                    Pera – Limon – Naranja
     Manzana – Limon                                           Pera – Limon – Kiwi
     Manzana – Naranja                                         Pera – Naranja – Kiwi
     Manzana – Kiwi                                              Manzana – Limon – Naranja
     Limon – Naranja                                              Manzana – Limon – Kiwi
     Limon – Kiwi                                                  Manzana – Naranja – Kiwi
     Naranja – Kiwi                                                Limon – Naranja – Kiwi

Como se puede observar, el orden de las frutas no es relevante ya que el valor del conjunto: (Pera – Manzana) es el mismo que el del conjunto: (Manzana – Pera), esto es: hay Manzana y hay Pera.

También se puede observar que tenemos 32 valores diferentes, que son todos los subconjuntos posibles que se pueden obtener a partir del CONJUNTO REFERENCIAL que contiene a todas las frutas. En general, a partir de un tipo referencial de N valores posibles se forman conjuntos con 2^N valores. Por tanto, en nuestro caso para los 5 valors posibles del tipo enumerado Frutas tenemos 32 subconjuntos posibles.

La utilización de los conjuntos tiene como ventajas adicionales que todos los lenguajes disponen de las operaciones básicas entre conjuntos: Unión, Intersección, etc. Así, para actualizar las existencias de frutas de nuestra frutería cuando llega un camión con nuevas frutas, sólo se necesita realizar la operación UNIÓN entre los conjuntos de frutas de la frutería y frutas del camión.

Una aplicación típica de los conjuntos aparece en el tratamiento del estado y las alarmas de los equipos conectados a un computador (modem, impresora, ratón, teclado, etc.). En general, lo que más nos interesa saber es si hay o no hay conexión, sobretemperatura, error de paridad, atasco, etc. A lo largo de este capitulo se detallan más ejemplos de la utilidad de los conjuntos.

9.6.1 Definición de tipos conjunto
Para manejar este tipo de datos en Modula-2 se pueden definir tipos conjunto. La definición de un tipo conjunto se realiza tomando como base o conjunto referencial el conjunto de todos los valores posibles de un tipo ordinal definido previamente. Por analogía con el vocabulario de conjuntos, llamaremos tipo referencial a este tipo base. En el ejemplo anterior el tipo referencial es el tipo Frutas.

La definición de un tipo conjunto en Modula-2 utiliza las palabras clave SET OF, seguidas de la indicación del tipo referencial en el que está basado, de la siguiente forma:

TipoConjunto = SET OF TipoReferencial

Por ejemplo:

TYPE Existencias = SET OF Frutas;
         Caracteres = SET OF CHAR;
          Letras = SET OF [ "A" .. "Z" ];
          Digitos = SET OF RangoDigitos;
          Errores = SET OF (Exceso, Defecto);
          Mezcla = SET OF Color;
          Tabla = SET OF [ 1 .. 49 ];

Como se puede observar, en la definición se pueden utilizar tipos ordinales definidos anteriormente en el programa o tipos predefinidos del lenguaje, o enumeraciones o subrangos de cualquiera de ellos introducidos en la propia definición. No se pueden declarar conjuntos sobre valores reales, ya que no es un tipo ordinal y no se pueden declarar subrangos sobre ellos.

La definición del lenguaje Modula-2 establece que el compilador determine limitaciones para el tipo referencial. La limitación es que los ordinales del rango de valores del referencial deben estar comprendidos entre cero y un límite positivo particular para cada compilador. Este límite suele permitir definir conjuntos sobre el tipo CHAR, pero no es posible, en general, definir conjuntos directamente sobre los tipos CARDINAL o INTEGER.

Formalmente, la declaración de un tipo conjunto tiene la siguiente sintasix:

Tipo_conjunto ::= SET OF Tipo_simple

Tipo_simple ::=
         Identificador_de_tipo |
         Tipo_enumerado |
         Tipo_subrango

9.6.2 Construcción de conjuntos
A partir de la definición de los tipos conjuntos se pueden hacer declaraciones y posteriormente trabajar con ellas. Por ejemplo:

VAR mercadoHoy: Existencias;
        boleto: Tabla;
        listaLetras: Letras;
        paleta: Mezcla;

Para asignar valores a las variables es necesario, obviamente, indicar el valor de tipo conjunto que se desa asignar. Una forma de expresar un valor de tipo conjunto es indicar expresamente cuáles son sus elementos. Esto se hace mediante una expresión de construcción de conjunto, en la que se enumeran, entre llaves ({}), los elements del conjunto a incluir, separados por comas (,) y precedidos por el identificador del tipo del conjunto al que pertenecen los elementos. Por ejemplo:

mercadoHoy := Existencias{ Kiwi, Limon, Pera };
…..
listaLetras := Letras{ };
…..
paleta := Mezcla{ Rojo };

Si no se indica ningún elemento dentro de los corchetes, el conjunto asignado es el conjunto vacío. La lista de elementos puede ser tan larga como sea necesario y el orden en que se relacionan es irrelevante. Para abreviar, si hay varios elementos consecutivos basta con indicar el primero y el último separados por dos puntos seguidos (..). Por ejemplo:

listaLetras := Letras{ "A", "D", "H" .. "M" };
…..
boleto := Tabla{ 1 .. 5, 10 .. 15, 20 .. 25 };

En el caso de que el ordinal del primer elemento sea superior al del último, el rango que se está indicando es un rango vacío. Por ejemplo:

paleta := Mezcla{ Azul .. Rojo };

asigna el conjunto vacío a la variable paleta. Como se puede obsevar existen distintas formas de indicar el mismo conjunto. Por ejemplo:

mercadoHoy := Existencias{ Limon, Naranja .. Kiwi, Pera };
mercadoHoy := Existencias{ Pera, Limon, Naranja, Kiwi };
mercadoHoy := Existencias{ Pera, Limon .. Kiwi };
mercadoHoy := Existencias{ Pera, Limon .. kiwi, Naranja };

Todas estas expresiones dan como resultado el mismo conjunto. En el último caso, aunque se repite la inclusión del elemento Naranja, este sólo aparece una vez en el conjunto asignado a la variable mercadoHoy.

Las versiones modernas de Modula-2 permiten que los valores de los elementos del conjunto que se construye puedan ser dados en forma de expresión. Por ejmplo:

boleto := Tabla{ x .. (x+y) };

Esta misma manera de formar conjuntos también se puede utilizar para declarar constantes de tipo conjunto. Por ejemplo:

CONST
           Verde = Mezcla{ Azul, Amarillo };
           todasLasFrutas = Existencias{ Pera .. Kiwi };
           boletoInicial = Tabla{ };

En este caso, si se usarán expresiones para los elementos del conjunto, deberán ser expresiones constanes.

Las reglas BNF para las expresiones de construcción de conjuntos son las siguientes:

Construcción_de_conjunto ::=
      Identificador_de_tipo { Lista_de_elementos }
Lista_de_elementos ::= [ Elementos {, Elementos }]
Elementos ::= Expresión_constante [.. Expresión_constante]

Fundamentos de programación con Módula-2 (19)

8.2.5 Ejemplo: Tabular la serie de Fibonacci
El procedimiento de imprimir tabulando desarrollado en el ejemplo de imprimir tabla de números primos, puede aprovecharse para imprimir en forma de tabla otras series de valores. Por ejemplo, podemos tabular la serie de Fibonacci, que ya se describió en el Tema 6. Lo que necesitamos ahora es sustituir las sentencias de escritura usadas en aquel ejemplo

                   WriteInt( termino, 10 );
                   WriteLn

por una llamada al procedimiento

                   ImprimirTabulando( termino )

y, por supuesto, copiar en la parte de declaraciones la definición del procedimiento de tabular, y añadir al comienzo del programa la inicialización del contador columnas. El programa completo aparece listado a continuación:

(*****************************************************************************************
*
*    Programa: Fibonaci
*
*    Descripción:
*      Este programa imprime todos los términos de la serie
*      de Fibonaci, dentro del rango de valores positivos del tipo INTEGER:
*        1 .. MAX(INTEGER)
*      Se imprime tabulando a cuatro columnas
*
*****************************************************************************************)
MODULE Fibonaci;

(*========================================================================================
    IMPORTACIÓN Y DECLARACIONES DEL PROGRAMA
========================================================================================*)
  FROM InOut IMPORT WriteInt, WriteLn;

  VAR
    columna: INTEGER;            (* columna a imprimir *)
    termino: INTEGER;            (* término de la serie *)
    anterior: INTEGER;            (* término anterior *)
    aux: INTEGER;

  PROCEDURE ImprimirTabulando( k: INTEGER );
  (* Imprimir ‘k’ tabulando a 4 columnas de 10 caracteres *)
  BEGIN
    IF columna > 4 THEN
      columna := 1;
      WriteLn
    END;
    WriteInt( k, 10);
    INC( columna )
  END ImprimirTabulando;

BEGIN

(*=======================================================================================
    PARTE EJECUTABLE DEL PROGRAMA
=======================================================================================*)
  (*– iniciar la tabulación –*)
    columna := 1;
  (*– generar el comienzo de la serie –*)
    anterior := 0;
    termino := 1;
    ImprimirTabulando( anterior );
    ImprimirTabulando( termino );
  (*– generar el resto de la serie –*)
    WHILE MAX(INTEGER)-termino >= anterior DO
      aux := termino + anterior;
      anterior := termino;
      termino := aux;
      ImprimirTabulando( termino )
    END;
  WriteLn
END Fibonaci.

El resultado de la ejecución en una máquina con números enteros de 16 bits ( MAX(INTEGER) = 32767 ) es el siguiente:

   0         1         1         2
   3         5         8        13
  21        34        55        89
 144       233       377       610
 987      1597      2584      4181
6765     10946     17711     28657


8.2.6 Desarrollo para reutilización
Para aplicar de manera eficaz las técnicas de reutilización de software es preciso pensar en las posibles aplicaciones de un cierto subprograma en el momendo de especificarlo, con independencia de las necesidades particulares del programa que se está desarrollando en ese momento.

Esta estrategia de desarrollo tiene ventajas e incovenientes. La principal ventaja es que se amplía el conjunto de aplicaciones en que se podrá reutilizar más adelante el subprograma que se está desarrollando ahora. Su principal incoveniente es que será más costoso hacer el desarrollo del subprograma planteado como operación de uso general, que planteado como operación particular, hecha a medida del programa que lo utiliza en este momento.

En el ejemplo del árbol de Navidad, nos encontramos con que al buscar analogías entre distintas operaciones para resolverlas con un subprograma común, estábamos generalizando al mismo tiempo dichas operaciones, estableciendo parámetros que permitían particularizarla para cada caso.

En el caso de subprogramas planteados simplemente con el fin de limitar el nivel de detalle en una sección determinada de un programa, no se siente esta necesidad de generalizar,  es más fácil de plantear la operación particularizada para las necesidades de ese momento.

En el ejemplo de tabular las series de valores, se ha planteado de entrada la operación de tabulación de manera que impone tanto el número de columnas como el ancho de cada una. Si queremos escribir un subprograma de tabulación de resultados que sea realmente de uso general, convendría dejar libertad para fijar las características del listado como parámetros modificables, que se puedan particularizar para cada caso.

De esta manera se podría haber ampliado el campo de aplicación del subprograma de tabular si el número de columnas y el ancho de cada una fuesen parámetros variables. Además, para simplificar el uso del procedimiento de tabulación se podrían agrupar todas las acciones de inicializacióon en una sola acción abstracta, invocada como subprograma, en que se fijen los parámetros particulares del listado. La especificación de esta acción inicial podría ser:

         PROCEDURE IniciarTabulacion( columnas, ancho: INTEGER );
         (* Iniciar la tabulación on los parámetros indicados *)

Para ilustrar esta técnica, modificaremos el programa de tabular la serie de Fibonacci de acuerdo con lo expuesto, decidiendo el formato del listado (7 columnas de 6 caracteres) desde el programa principal. El programa modificado es el siguiente:

(******************************************************************************************************************************************
*
*              Programa: Fibonac2
*
*              Descripción:
*
*                Este programa imprime todos los términos de la serie de Fibonaci, dentro del rango de valores positivos
*                del tipo INTEGER: 1 ..MAX(INTEGER). Se imprime tabulando a siete columnas
*
*
*
******************************************************************************************************************************************)

MODULE Fibonac2;
   FROM InOut IMPORT WriteInt, WriteLn;

   VAR
      TABcolumna: INTEGER;                                 (* columna a imprimir *)
      TABultima: INTEGER;                                  (* última columna *)
      TABancho: INTEGER;                                   (* ancho de cada columna *)

   VAR
      termino: INTEGER;                                  (* término de la serie *)
      anterior: INTEGER;                                       (* término anterior *)
      aux: INTEGER;              

PROCEDURE IniciarTabulacion( columnas, ancho: INTEGER );
   (* Iniciar la tabulación con los parámetros indicados *)
BEGIN
   TABultima := columnas;
   TABancho := ancho;
   TABcolumna := 1;
END IniciarTabulacion;

PROCEDURE ImprimirTabulando( k: INTEGER );
  (* Imprimir ‘k’ tabulando a TABultima columnas de TABancho caracteres *)
BEGIN
   IF TABcolumna > TABultima THEN
      TABcolumna := 1;
      WriteLn
   END;
   WriteInt( k, TABancho );
   INC( TABcolumna );
END ImprimirTabulando;

BEGIN
  (*– inicializar la tabulación –*)
      IniciarTabulacion( 7, 6 );
  (*– generar el comienzo de la serie –*)
     anterior := 0;
     termino := 1;
     ImprimirTabulando( anterior );
     ImprimirTabulando( termino );
  (*– generar el resto de la serie –*)
     WHILE MAX(INTEGER)-termino >= anterior DO
        aux := termino + anterior;
        anterior := termino;
        termino := aux;
        ImprimirTabulando( termino )
     END;
   WriteLn
END Fibonac2.

y el resultado de la ejecución es el siguiente:

    0     1     1     2     3     5     8
   13    21    34    55    89   144   233
  377   610   987  1597  2584  4181  6765
10946 17711 28657

Conviene comentar algunos aspectos de estilo utilizados en este ejemplo. Como se emplean variables globales para la tabulación, se ha procurado separar la declaración de estas variables de la declaración de las variables propias del programa principal. Además, se han nombrado todas las variables usadas por los procedimientos de tabulación, empezando sus nombres con el prefijo TAB, para evitar confusiones.

En realidad esto es un recurso artificioso para separar las distintas partes del programa. Este recurso se ha utilizado de momento porque todavía no se ha presentado el mecanismo de definición de módulos en Modula-2. Usando el mecanismo de módulos se pueden desarrollar subprogramas reutilizables, escritos en forma realmente independiente, de una manera mucho más adecuada.

8.2.7 Desarrollo ascendente
La metodología de desarrollo ascendente (en inglés "Bottom-Up") consiste en ir creando subprogramas que realicen operaciones significativas de utilidad para el programa que se intenta construir, hasta que finalmente sea posible escribir el programa principal, de manera relativamente sencilla, apoyándose en los subprogramas desarrollados hasta ese momento.

La técnica tiene una cierta analogía con el desarrollo de subprogramas pensando en su reutilización posterior. Al hablar de desarrollo para reutilización se ha dicho que los subprogramas podían surgir en el proceso de refinamiento de un programa concreto, al identificar ciertas operaciones, pero debían definirse pensando en futuras aplicaciones. En este caso se trata de que la identificación de las operaciones no surga de un proceso de descomposición o refinamiento de alguna acción particular, sino simplemente pensando en el programa que se desarrolla, casi como una más de las posibles aplicaciones futuras.

Como ejemplo desarrollaremos un programa que opere como una calculadora, pero con fracciones. Una fracción se compondrá de un numerador y un denominador enteros. La calculadora podrá sumar, restar, multiplicar o dividir fracciones, y los resultados los presentará con la fracción en forma simplificada, dividiendo por los factores comunes al numerador y al denominador.

Con independencia de los detalles de operación de la calculadora, pueden desarrollarse incialmente procedimientos útiles para esta aplicación; en particular procedimientos para realizar cálculos con fracciones, así como leerlas o imprimirlas. En el siguiente listado se presenta una colección apropiada de procedimientos, sobre los cuales se podrá desarrollar luego el programa principal de la calculadora

FROM InOut IMPORT
   WriteString, Write, WriteInt, WriteLn, Read, ReadInt;

   . . . . . . . .

   PROCEDURE ReducirFraccion( VAR n, d: INTEGER );
   (* Simplificar la fracción n/d *)
      VAR divisor: INTEGER;
   BEGIN
      divisor := 2;
      WHILE (divisor <= n) AND (divisor <=d) DO
         WHILE (n MOD divisor = 0) AND (d MOD divisor = 0) DO
            n := n DIV divisor;
            d := d DIV divisor
         END;
         INC( divisor )
      END
   END ReducirFraccion;

   PROCEDURE SumarFracciones( n1, d1, n2, d2: INTEGER; VAR n3, d3: INTEGER );
   (* n3’/d3′ = n1/d1 + n2/d2 *)
   BEGIN
      n3 := n1*d2 + n2*d1;
      d3 := d1*d2;
      ReducirFraccion( n3, d3 )
   END SumarFracciones;

   PROCEDURE RestarFracciones( n1, d1, n2, d2: INTEGER; VAR n3, d3: INTEGER );
   (* n3’/d3′ = n1/d1 – n2/d2 *)
   BEGIN
      SumarFracciones( n1, d1, -n2, d2, n3, d3 )
   END RestarFracciones;

   PROCEDURE MultiplicarFracciones( n1, d1, n2, d2: INTEGER; VAR n3, d3: INTEGER );
   (* n3’/d3′ = n1/d1 * n2/d2 *)
   BEGIN
      n3 := n1*n2;
      d3 := d1*d2;
      ReducirFraccion( n3, d3 )
   END MultiplicarFracciones;

   PROCEDURE DividirFracciones( n1, d1, n2, d2: INTEGER; VAR n3, d3: INTEGER );
   (* n3’/d3′ = n1/d1 / n2/d2 *)
   BEGIN
      n3 := n1*d2;
      d3 := d1*n2;
      ReducirFraccion( n3, d3 )
   END DividirFracciones;

   PROCEDURE LeerFraccion( VAR n, d: INTEGER );
   (* Lee la fracción y la simplifica *)
   BEGIN
      ReadInt( n ); Write( ‘/’ );
      ReadInt( d ); Write( ‘ ‘ );
      ReducirFraccion( n, d )
   END LeerFraccion;

   PROCEDURE  EscribirFraccion( n, d: INTEGER );
   (* Imprime la fracción como n’/d’ *)
   BEGIN
   WriteInt( n, 1 );
   Write( ‘/’ );
   WriteInt( d, 1 )
   END EscribirFraccion;

Contando con esos procedimientos se puede ahora desarrollar el programa principal de la calculadora, que se presenta en el programa Fraccion. En este ejemplo se supone que cada operación se realiza entre un valor acumulado y un nuevo operando. La operación se inicia con una tecla de operación, y a continuación se introduce el valor del operando. Las operaciones previstas son +, -, *, /. Además habrá teclas de operación para imprimir el resultado acumulado (=) y para iniciar una nueva serie de operaciones (espacio en blanco). La tecla F marcará el fin del funcionamiento del programa.

(********************************************************************************
*
*   Programa: Fraccion
*
*   Descripción:
*      Este programa es una calculadora que suma, resta, multiplica o
*      divide fracciones
*
*********************************************************************************)
MODULE Fraccion;

. . . . <<definiciones de los procedimientos>> . . . . 

   VAR
      num, den: INTEGER;                    (* acumulador *)
      nn, dd: INTEGER;                         (* operando *)
      operacion: CHAR;                         (* tecla de operación *)

PROCEDURE LeerOperacion( VAR op: CHAR );
(* Lee la tecla de operación *)
BEGIN
   Write( ‘?’ ); Read( op );
   op := CAP( op );
   Write( op ); Write( ‘ ‘ );
END LeerOperacion;

BEGIN
   num := 0;
   den := 0;
   LeerOperacion( operacion );
   WHILE operacion <> ‘F’ DO
      IF operacion = ‘+’ THEN
         LeerFraccion( nn, dd );
         SumarFracciones( num, den, nn, dd, num, den )
      ELSIF operacion = ‘-‘ THEN
         LeerFraccion( nn, dd );
         RestarFracciones( num, den, nn, dd, num, den )
      ELSIF operacion = ‘*’ THEN
         LeerFraccion( nn, dd );
         MultiplicarFracciones( num, den, nn, dd, num, den )
      ELSIF operacion = ‘/’ THEN
         LeerFraccion( nn, dd );
         DividirFracciones( num, den, nn, dd, num, den )
      ELSIF operacion = ‘ ‘ THEN         (* Iniciar nuevos cálculos *)
         LeerFraccion( num, den );          (* con num y den *)
      ELSIF operacion = ‘=’ THEN
         WriteString( ‘           ‘);
         EscribirFraccion( num, den );
      ELSIF operacion = ‘F’ THEN
         (* fin de operacion *)
      ELSE
         WriteString( ‘Pulse +, -, *, /, ESPACIO, =, o F’ )
      END;
      WriteLn;
      IF operacion <> ‘F’ THEN
         LeerOperacion( operacion )
      END
   END
END Fraccion.

Un posible ejemplo de la ejecución del programa es el siguiente: 

?  5/20
?=            1/4
?+ 3/5
?- 2/4
?=            7/20
?* 5/6
?=            7/24
?F

En esta aplicación de la técnica de desarrollo ascendente se puede apreciar que el desarrollo inicial de procedimientos para realizar cálculos con fracciones nos ha permitido disponer de una extensión del lenguaje Modula-2, equivalente a definir el tipo FRACCION. Podríamos decir que los procedimientos de cálculo constituyen en conjunto una máquina virtual de operar con fracciones, sobre la cual se ha desarrollado el programa de la calculadora. El desarrollo es ascendente porque primero se han construído los subprogramas, de nivel inferior, y luego el programa que los usa, de nivel superior.

8.3 Programas robustos.

La corrección de un programa exige que los resultados sean los esperados, siempre que el programa se ejecute con unos datos de entrada aceptables. La cuestión que nos ocupa en este momento es: ¿Cuál debe ser el comportamiento del programa si los datos son incorrectos?

Un programa se dice que es robusto si su operación se mantiene en condiciones controladas aunque se le suministren datos erróneos.

8.3.1 Programación a la defensiva
La postura más cómoda desde el punto de vista del programador es declinar toda responsabilidad en el caso de que los datos no sean válidos. Si los datos de entrada no cumplen con los requisitos previstos, el programa puede entonces hacer cualquier cosa. Es frecuente que un programa se escriba sin tener en cuenta la posibilidad de que los datos no sean esperados, pues con ello se simplifica su desarrollo.

Sin embargo esta postura no es admisible en la práctica. Como cualquier otra actividad humana, la escritura y uso de programas está sujeta a errores, y es importante conseguir que las consecuencias de esos errores no sean demasiado graves. Por ejemplo, un programa de gestión de un almacén deberá prever que se notifique la retirada de más cantidad de un producto que la anotada como existencias. En este caso el programa deberá hacer algo "razonable", tal como emitir un mensaje de aviso y obligar a repetir la operación, o simplemente asumir que el valor de las existencias estaba equivocado, y preguntar por el valor real de las existencias, o alguna otra cosa similiar. Lo que no parece "razonable" es anotar un valor negativo para las existencias sin dar ningún aviso, o, en general, seguir operando con valores manifiestamente erróneos que podrían dar lugar más adelante a una parada indeseada del programa ("aborto") al intentar ejecutar alguna instrucción de máquina inadmisible con esos valores.

Otro ejemplo ilustrativo puede ser el de un programa para calcular el valor medio de una serie de datos, dividiendo la suma de todos por el número de datos introducidos. Cabe la posibilidad de que no se introduzca ningún dato, lo cual dará lugar a un intento de realizar una división por cero, que en muchos casos produce el "aborto" del programa. Si el cálculo de la media es lo único que hace el programa, el efecto no parece muy grave, pero si este cálculo es parte de las operaciones que realiza, por ejemplo, el programa de control de una central nuclear, los resultados pueden conducir a una catástrofe mundial. Lo realmente importante es detectar los errores en cuanto se produzcan, y poder así programar operaciones de corrección o tratamiento apropiadas para estas situaciones excepcionales.

La llamada programación a la defensiva (en inglés, "defensive programming") consiste en que cada programa o subprograma esté escrito de manera que desconfíe sistemáticamente de los datos o argumentos con que se le invoca, y devuelva siempre como resultado:

a) el resultado correcto, si los datos son admisibles, o bien
b) una indicación precisa de error, si los datos no son admisibles.

Lo que no debe hacer nunca el programa es devolver un resultado como si fuese normal, cuando en realidad es erróneo, ni "abortar". Esto da lugar a una propagación de errores, que puede aumentar la gravedad de las consecuencias, y hacer que la identificación del fallo del programa resulte mucho más difícil, ya que el efecto se puede manifestar sólo más adelante, en otra parte del programa sin relación aparente con la que falló.

La mejora de la robustez del programa tiene como contrapartida una cierta pérdida de eficiencia, al tener que hacer comprobaciones adicionales. Si la eficiencia es un factor decisivo, algunas de estas comprobaciones pueden eliminarse en la versión final del programa, cuando se determine con seguiridad que el programa no contiene errores.

Consideremos el caso de una función para calcular el factorial de un número:

        n! = 1 x 2 x 3 x … x n

El código de la función podría ser:

PROCEDURE Factorial( n: INTEGER ): INTEGER;
   VAR k, f: INTEGER;
BEGIN
   f := 1;
   FOR k := 2 TO n DO
      f := f * k
   END;
   RETURN f
END Factorial;

Esta función no es robusta. El factorial sólo está definido para valores de n positivos, incluido el cero, cuyo factorial, por convenio vale 0! = 1. Para valores negativos el factorial no está definido, y sin embargo la función codificada en la forma anterior devuelve resultado 1, indistinguible del resultado correcto correspondiente a 0! o 1!.

Lo que hace falta es devolver una indicación clara de error para argumentos negativos. Una forma de hacerlo podría ser devolver un resultado cero o negativo en estos casos, ya que ese resultado no puede coincidir con el factorial de ningún número. La función se recodificaría en la forma:

PROCEDURE Factorial( n: INTEGER ): INTEGER;
   VAR k, f: INTEGER;
BEGIN
   IF n < 0 THEN
      f := 0
   ELSE

      f := 1;
      FOR k := 2 TO n DO
         f := f * k
      END;
   END;
   RETURN f
END Factorial;

En realidad la función sigue sin ser del todo robusta, ya que no se ha previsto la posibilidad de que el factorial que se intenta calcular exceda del rango admisible de valores de tipo INTEGER. Esto ocurre fácilmente incluso para valores relativamente pequeños del argumento (p.ej., 20! = 2432902008176640000). En la sección siguiente se presenta una versión más robusta de esta función.

8.3.2 Tratamiento de excepciones
Ante la posibilidad de errores en los datos con que se opera, hay que considerar dos actividades diferentes:

1) Detección de la situación de error.
2) Corrección de la situación de error.

Si una operación se ha escrito como subprograma, la programación a la defensiva recomienda que la primera actividad (detección del posible error) se haga dentro del subprograma, sin confiar en que quienes usen el subprograma lo invoquen siempre con datos correctos.

La segunda actividad, sin embargo, no puede realizarse, en general, dentro del subprograma, ya que el tratamiento adecuado de la situación excepcional podrá ser diferente en cada invocación. Lo que ha de hacer el subprograma es devolver una indicación precisa del error, y dejar que sean los programas que lo invocan quienes decidan cómo actuar frente al error en cada caso.

El esquema típico de un fragmento de programa con tratamiento de excepciones será:

. . . .
Operación( argumentos );
IF hubo error THEN
   tratamiento del error
END;
. . . .

Existen varios esquemas de programa posibles para tratamientos de errores. Un modelo recomendado es el de terminación. En este modelo, si se detecta error en una sección o bloque del programa, la acción de tratamiento del error reemplaza al resto de las acciones pendientes de dicha sección, con lo cual tras la acción correctora se da por terminado el bloque. En algunos lenguajes de programación modernos, tales como el lenguaje Ada, existen construcciones o sentencias adecuadas para programar este esquema. En Modula-2 no existen sentencias especiales de manejo de excepciones, y hay que programarlas con los medios disponibles. Un subprograma desarrollado en Modula-2 siguiendo el modelo de terminación podría programarse según el siguiente esquema:

PROCEDURE Operación( argumentos );
BEGIN
   … acción 1
   IF error THEN
      … tratamiento 1
      RETURN
   END;
   … acción 2
   IF error THEN
      … tratamiento 2
      RETURN
   END;
   . . . .
END Operación;

Aplicaremos este esquema a una variante mejoradade la función para calcular el factorial de un número, detectando la situación de exceso de capacidad ("overflow"), y devolviendo un valor negativo en ese caso.

PROCEDURE Factorial( n: INTEGER ): INTEGER;
   VAR k, f: INTEGER;
BEGIN
   IF n < 0 THEN
      RETURN 0
   END;
   f := 1;
   FOR k := 2 TO n DO
      IF f > MAX(INTEGER) DIV k THEN
         RETURN -1
      END;
      f := f*k
   END;
   RETURN f
END Factorial;

Esta función opera de manera robusta sea cual sea el rango de enteros de la máquina. Si se sabe de antemano cuál es dicho rango, se podría aumentar algo la eficiencia determinando por anticipado cuál es el mayor valor para el que se puede calcular el factorial, y detectando directamente si el valor del argumento excede de dicho límite, definido como parámetro constante.

El futuro del wireless (artículo de revista)

Probamos qué hay de cierto en las bondades del borrador del estándar 802.11n

Los teóricos 300 Mbps de régimen de transferencia compiten con las velocidades de las redes cableadas Ethernet, aunque todavía están muy lejos de las prestaciones de la tecnología Gigabit. Su mayor virtud sigue siendo la facilidad de instalación y la ausencia de cables.

Desde que apareció la primera tecnología para la implementación de redes inalámbricas, la evolución en lo que respecta a eficiencia y velocidad de transmisión se refiere ha sido considerable. El objetivo es claro: poder competir con la velocidad o, mejor dicho, régimen de transferencia de las redes cableadas, es decir Ethernet (10/100) y Gigabit (1000). Conceptualmente, no existe ninguna diferencia entre una red con cables (coaxial, fibra óptica, etcétera) y una inalámbrica. La distinción está en que las redes inalámbricas transmiten y reciben datos a través de ondas electromagnéticas, lo que supone la eliminación del uso de cables y, por tanto, una total flexibilidad en las comunicaciones.
   Hasta ahora, las ventajas de la infraestructuras inalámbricas respecto a las cableadas estaban claras: sencilles de instalación y movilidad de los equipos conectados por medio de esta tecnología. Por el contrario, el principal inconveniente se centraba en su capacidad de transmisión y en las posibles interferencias propias del medio en el que se transmiten. Si las redes convencionales eran capaces de transferir a un régimen teórico de 100 Mbps y 1.000 Mbps, los últimos productos basados en tecnología inalámbrica tan sólo alcanzan los 54 Mbps. Esta disconformidad teórica y práctica supone un salto demasiado grande cuando llega el momento de realizar algunas operaciones, por ejemplo, la transferencia de archivos de vídeo, streaming o soportar con soltura la transferencia de música con obstáculos de por medio, el escenario más habitual.
   Aunque originalmente este tipo de transmisión fue desarrollada con el objetivo de cubrir las necesidades dentro del ámbito profesional, la realidad es que su fácil instalación y las posibilidades que ofrece a los usuarios (las ya mencionadas de streaming de audio y vídeo) han abierto un nicho de mercado demasiado atractivo para los fabricantes como para reninciar a él. Aunque hasta ahora las velocidades de transmisión han ido elevándose paulatinamente, las necesidades han aumentado de forma exponencial. En este sentido, las expectativas generales del nuevo estándar de comunicación sin cables prometen elevados ratios de transferencia y una mejora notable en la cobertura y estabilidad de la señal WiFi.

Las tecnologías anteriores
Hasta la próxima ratificación del estándar 802.11n, hemos convivido con las especificaciones a, b y g. A continuación, vamos a mostrar las características de todas ellas para poder establecer comparaciones con la última en llegar.
   Las dos primeras en aparecer fueron las denominadas 802.11a  y b. La primera de ellas es la más utilizada en los EE UU. Esta revisión, certificada en 1999, opera en la banda de 5 GHz con una velocidad máxima de 54 Mbps, lo que lo convierte en un estándar práctico para entornos con velocidades reales de aproximadamente 20 Mbps. La velocidad de datos se reduce a 48, 36, 24, 18, 12, 9 o 6 Mbps en caso necesario. Es incompatible con el estándar 802.11b, lo que supuso un problema para los usuarios, ya que había que elegir entre un tipo de dispositivo u otro.
   La elección de la banda de los 5 GHz no es casual. Dado el elevado uso de la de 2,4 GHz (empleada, entre otros aparatos, por los teléfonos inalámbricos y los hornos microondas), su utilización representa la disminución de interferencias. Sin embargo, también cuenta con desventajas, dado que restringe el uso de los equipos 802.11a a únicamente puntos en línea de vista, con lo que se hace necesario la instalación de un mayor número de puntos de acceso. A su vez, implica que los equipos que trabajan bajo este estándar no pueden penetrar tan lejos como los del estándar 802.11b, puesto que sus ondas son más fácilmente absorbidas. La relación entre el régimen de transmisión y la distancia entre emisor y receptor vienen a ser las siguientes: a 30 metros, 54 Mbps; a 300 metros, 6 Mbps en entornos diáfanos o exteriores; mientras, en interiores, a 12 metros, mantiene los 54 Mbps, y a 90 metros se reduce a 6 Mbps.
   La especificación 802.11b trabaja sobre la banda de los 2,4 GHz y la tasa de transferencia teórica es de 11 Mbps. No obstante, se ve reducida hasta los 6 Mbps en condiciones ideales y hasta los 2 Mbps en un entorno de oficina.
   Ante la imposibilidad de estos estándares de interoperar y dado que las necesidades del mercado no se veían cubiertas con los teóricos 11 Mbps del 802.11b, se desarrolló la especificación 802.11g. Al igual que el estándar 802.11b, utiliza la banda de 2,4 GHz, pero opera a una velocidad teórica máxima de 54 Mbps, unos 24,7 de velocidad real de transferencia, similar a la del estándar 802.11a. Es compatible con el estándar b y utiliza las mismas frecuencias. Buena parte de su proceso de diseño tuvo como objetivo hacer compatibles los dos estándar g y la presencia de nodos bajo el b reduce significativamente la velocidad de transmisión.

Las características de la n
La tecnología 802.11n comenzó su andadura en enero de 2004, cuando la asociación internacional IEEE creó un grupo de trabajo para su desarrollo. A principios de este año aprobó el borrador 2.0 y es previsible que en el 2008 se ratifique como estándar. No obstante, ya existen en el mercado productos (como los que analizamos en estas páginas) que funcionan bajo Wireless-N y conocidos como pre-802.11n. Las principales novedades que aporta son una velocidad inicial de hasta 3000 Mbps en el borrador, aunque es previsible que la versión final ascienda a 600 Mbps; una mayor cobertura y fiabilidad gracias a la incorporación de la tecnología MIMO (Multiple-Input, Multiple-Output), que utiliza múltiples antenas de emisión y recepción en las transmisiones; y la capacidad de transferir en las frecuencias de 2,4 y 5 GHz de forma simultánea.
   Gracias a estas características y al empleo de distintas antenas, se puede regular la potencia de emisión para llegar mejor y con mayor calidad de señal hasta cada cliente. Así, es posible superar obstáculos físicos que con otros equipos supondría una gran pérdida de la calidad de la señal, uno de los grandes problemas de las tecnologías utilizadas hasta ahora.
   El problema es que los productos que van lanzándose al mercado no pasan por el momento de ser meras aproximaciones a lo que será el estándar definitivo, todavía por confirmar. Sus prestaciones serán similares, pero algunos aspectos, como la total compatibilidad con 802.11b/g, aún tienen puntos por definir. Este detalle es de especial importancia, ya que lo que se pretende es desarrollar un nuevo estándar compatible <<hacia atás>>, es decir, que pueda aprovechar los productos e infraestructuras existentes hoy en día.

La seguridad, asignatura aprobada
   La seguridad de las redes inalámbricas, bajo la tecnología que sea, es uno de los problemas más graves a los cuales se enfrenta la tecnología WiFi. Un elevado porcentaje de redes son instaladas por administradores de sistemas o usuarios con pocos conocimientos debido a la simplicidad de su implementación, por lo que no se detienen demasiado en todo aquello relacionado con la seguridad, convirtiendo, por tanto, sus redes en infraestructuras abiertas, en las que circula todo tipo de información sin la protección requerida.
   Afortunadamente, existen varias alternativas para garantizar la seguridad de estas redes. Aunque podemos instalar cortafuegos para detectar cualquier intrusión en ellas, los medios más comunes se basan en la utilización de protocolos de seguridad de datos específicos para entornos WiFi. Estos protocolos se basan en la encriptación de datos. Los ejemplos más conocidos son WEP y WPA, que se encargan de la autentificación, integridad y confidencialidad, y son proporcionados por los fabricantes de los propios dispositivos inalámbricos. Lógicamente, estos sistemas están recogidos en las normas del conjunto de protocolos IEEE 802.1X. No obstante, el denominado WPA2, que es una mejora del WPA, es el mejor protocolo de seguridad para WiFi en este momento. Para su utilización en un ordenador cargado con Windows XP, se requiere el Service Pack 2 y una actualización adicional.

Nuestras pruebas
En estas líneas, ya hemos comentado que el borrador debe ser ratificado, por eso, la incorporación de estos productos al mercado está siendo bastante lenta. A pesar de ello, algunos fabricantes han comenzado a comercializar productos bajo las especificaciones del borrador 802.11n. En este artículo lo hemos tenido acceso a ingenios que se sirven de este borrador (Netgear, Linsys Wireless, Conceptronic, D-Link, Asus y SMC), aunque otros fabricantes, como Belkin, 3Com o Edimax, cuentan con productos similares en el mercado.
   Para probar la efectividad del conjunto compuesto por un router inalámbrico y un adaptador USB o PCMCIA, decidimos crear dos escenarios para nuestras pruebas. Con el primero de ellos hemos pretendido saber cuál es el rendimiento que podemos alcanzar en condiciones ideales, es decir, sin interferencias externas. Sin embargo, la situación real no es ésta. Lo más habitual es que nos encontremos con interferencias de todo tipo, incluidas aquellas generadas por las instalaciones inalámbricas de nuestros vecinos, en caso de que esté emplazado en una casa, o de otros routers si se encuentra en una oficina. Por esta razón, hemos creado una segunda infraestructura inalámbrica que emitía datos sin cesar para comprobar el funcionamiento del dispositivo con la interferencia de una segunda red, de modo que nos ofrezca un entorno más próximo al que tendrán este tipo de dispositivos. Fernando Reinlein

Glosario

Ethernet: También se conoce como IEEE 802.3, es el estándar más popular para las LAN que se usa actualmente. Emplea una tecnología lógica de bus y una topología física de estrella o de bus. Ethernet permite datos a través de la red a una velocidad de 10 Mbps, en el caso de las redes más antiguas, y de 100 Mbps en el caso de las más actuales. A estas últimas también se las denomina Fast Ethernet. Mientras tanto, Gigabit es una extensión de este estándar que consigue una capacidad de transmisión de 1 gigabit por segundo.

MIMO: Acrónimo de Multiple-Input Multiple-Output (múltiple entrada, múltiple salida). Se refiere al uso de múltiples antenas tanto en el emisor como en el receptor para mejorar el rendimiento de los sistemas de comunicación vía radio. Ha conseguido llamar la atención de los fabricantes de productos inalámbricos porque aumenta significativamente la tasa de transmisión de datos sin un ancho de banda adicional. Lo consigue aumentando la eficiencia espectral de un sistema de comunicación por medio de la utilización del domino espectral (más bits por segundo por hertzio).

WEP: Acrónimo de Wired Equivalent Privacy. Es el sistema de cifrado incluido en el estándar IEEE 802.11 para cifrar la información que se transmite. Proporciona cifrado a nivel 2 y utiliza claves de 64 o 128 bits.

WPA: Acrónimo de Wi-Fi Protected Access. Es un sistema para proteger redes inalámbricas, creado para corregir las deficiencias del WEP (sobre todo de vulnerabilidad). Implementa la mayoría del estándar IEEE 802.11i (pendiente de publicación) y fue creado como una medida intermedia para ocupar el lugar de WEP mientras que se aprueba el 802.11i.

 

/:::::::::::::::::::::::::::::::::::::\
Artículo de revista PC actual. Año XVIII, número 200.

Fundamentos de programación con Módula-2 (18)

Tema 8
Metodología de Desarrollo de Programas (III)

 

Este tema completa el anterior, ampliando la metodología de desarrollo por refinamientos sucesivos con la posibilidad de usar subprogramas como técnica de abstracción.

A nivel metodológico, las funciones y procedimientos introducen la posibilidad de descomposición de un problema en subproblemas realmente independientes. De todas maneras se mantiene la unidad del programa, que sigue apareciendo como un único módulo principal en Modula-2.

8.1 Operaciones abstractas

Los subprogramas constituyen un primer paso hacia la metodología de programación basada en abstracciones. Los subprogramas permiten definir operaciones abstractas. El siguiente paso será la definición de tipos abstractos de datos, que se introducirán brevemente más adelante, en el Tema 14.

Una abstracción es una visión simplificada de una cierta entidad, de la que sólo consideramos sus elementos esenciales, prescindiendo en lo posible de los detalles. Las entidades que podemos abstraer para materializarlas como subprogramas son, en general, operaciones. Con la palabra operación englobamos tanto la idea de acción como la de función.

8.1.1 Especificación y realización
Al plantear operaciones abstractas habremos de definir dos posibles visiones. La visión abstracta o simplificada, y la visión detallada, completa. La visión abstracta es la que permite usar dicha operación sin más que conocer qué hace dicha operación. La visión detallada es la que define cómo se hace dicha operación, y permite que el procesador la ejecute. La primera visión representa el punto de vista de quienes han de utilizar la operación. Se dice que esa visión abstracta es la especificación o interfaz de la operación. La visión detallada representa el punto de vista de quien ha de ejecutar dicha acción, y se dice que expresa su realización o implementación. Resumiendo:

ESPECIFICACIÓN:          QUÉ HACE la operación (punto de vista de quien la invoca).

REALIZACIÓN:                CÓMO SE HACE la operación (punto de vista de quien la ejecuta).

En su forma más sencilla la especificación o interfaz consiste simplemente en indicar cuál es el nombre de la operación y cuáles son sus argumentos. En Modula-2 la especificación puede ser simplemente una cabecera de subprograma.

Esa forma simplificada de especificación indica solamente cuál ha de ser la sintasix o forma de uso de la operación. La especificación completa debe establecer también cuál es la semántica o significado de la operación. Para ello podemos añadir un comentario en que se indique qué relación hay entre los argumentos y el resultado de la operación.

La realización, por su parte, debe suministrar toda la información necesaria para poder ejecutar la operación. En Modula-2 la realización o implementación será la definición completa del subprograma, en forma de bloque de código.

Tomemos como ejemplo una función que calcule el máximo de dos números:

Especificación                { PROCEDURE Maximo2( a, b: INTEGER): INTEGER;
                                       (* Maximo2( a,b ) = Máximo de a y b *)

                                     / BEGIN
                                     |    IF a > b THEN
                                     |       RETURN a
Realización                    {    ELSE
                                     |       RETURN b
                                     |    END
                                     \END Maximo2;

Conociendo sólo la especificación podemos invocar la función, aunque no sepamos el detalle de cómo se realiza. Por ejemplo, podemos escribir:

alturaTotal := Maximo2( altura1, altura2 )

Si ahora sustituimos la realización de la función Maximo2 por otra diferente, tal como:

PROCEDURE Maximo2( a, b: INTEGER): INTEGER;
   VAR m: INTEGER;
BEGIN
   m := a;
   IF b > m THEN
      m := b
   END
   RETURN m
END Maximo2;

la especificación de la función sigue siendo la misma. La sentencia anterior que usaba la función sigue siendo siendo correcta:

alturaTotal := Maximo2( altura1, altura2 )

Con ello se pone de manifiesto la idea de que la especificación es una visión abstracta de qué hae la función, con independencia de los detalles de cómo lo hace.

8.1.2 Funciones. Argumentos
En programación la idea de función surge al aplicar el concepto de abstracción a las expresiones aritméticas. Una expresión representa un nuevo valor obtenido por cálculo a partir de ciertos valores ya conocidos que se usan como operandos.

Por ejemplo, el cubo de un número z se puede calcular multiplicando el número por sí mismo, en la forma zxzxz. De esta manera se puede obtener, por ejemplo, el volumen de un cubo escribiendo:

volumen := lado * lado * lado

La expresión lado * lado * lado suministra el valor del cubo del lado al evaluarla. Esta expresión puede verse de manera abstracta como una función, siendo el lado el dato de partida y el cubo el resultado obtenido. La abstracción de dicha expresión tendrá asociado un nombre que podamos identificar con el significado del cálculo, y que, obviamente, podría ser Cubo. Esto nos conduciría a la especificación:

PROCEDURE Cubo( z: REAL ): REAL;
   (* Cubo(z) = z^3 *)

Con esta especificación, el cálculo del volumen se reescribiría como:

volumen := Cubo( lado )

Esta visión abstracta prescinde de los detalles de cómo se calcula el resultado, con tal de que sea correcto, es decir, que se obtenga el cubo del argumento. La realización puede ser tan sencilla como:

PROCEDURE Cubo( z: REAL ): REAL
BEGIN
   RETURN z * z * z
END Cubo;

o tan artificiosa como:

PROCEDURE Cubo( z: REAL ): REAL;
   VAR c: REAL;
           k: INTEGER;
BEGIN
   c := 1.0;
   FOR k := 1 TO 3 DO
      c := c * z
   END;
   RETURN c
END Cubo;

Los operandos que intervienen en el cálculo del valor de la función y que pueden cambiar de una vez a otra se especifican como argumentos de dicha función. En el Tema anterior se han mencionado ya las dos formas disponibles en Modula-2 para el paso de los argumentos al subprograma que realiza el cálculo de la función. Si buscamos que el concepto de función en programación se aproxime al concepto matemático de función, el paso de argumentos debería ser siempre por valor. El concepto matemático de función es una aplicación entre conjuntos, cuyo cómputo se limita a suministrar un resultado, sin modificar el valor de los argumentos.

Aunque algunas veces, por razones de eficiencia, pueda ser aconsejable pasar por referencia argumentos de funciones, seguirá siendo deseable, para mantener la máxima claridad en el programa, que la llamada a la función no modifique el valor de los argumentos.

Desde el punto de vista de claridad del programa, y con independencia de cuál sea el mecanismo de paso de argumentos empleado, la cualidad más deseable al utilizar funciones es conseguir su transparencia referencial. Tal como se mencionó anteriormente, la transparencia referencial significa que la función devolverá siempre el mismo resultado cada vez que se la invoque con los mismos argumentos.

La transparencia referencial se garantiza si la realización de la función no utiliza datos exteriores a ella. Es decir, si no emplea:

  • Variables externas al subprograma, a las que se acceda directamente por su nombre, de acuerdo con las reglas de visibilidad de bloques.
  • Datos procedentes del exterior, obtenidos con sentencias de lectura.
  • Llamadas a otras funciones o procedimientos que no posean transparencia referencial. Las sentencias de lectura son en realidad un caso particular de éste.

Estas restricciones se cumplen en el ejemplo anterior del cálculo del cubo de un número. Las funciones que cumplen la cualidad de transparencia referencial y que no producen efectos laterales o secundarios se denominan funciones puras.

8.1.3 Acciones abstractas. Procedimientos
De manera similar a como las funciones pueden ser consideradas como expresiones abstractas, parametrizadas, los procedimientos pueden ser considerados como acciones abstractas, igualmente parametrizadas. Un procedimiento representa una acción, que se define por separado, y que se invoca por su nombre.

Como acciones abstractas, podemos tener dos visiones de un procedimiento. La primera es la visión abstracta o especificación, formada por la cabecera del procedimiento y una descripción de qué hace dicho procedimiento, y la segunda es la realización, en que se detalla, codificada en el lenguaje de programación elegido, cómo se hace la acción definida como procedimiento.

Como ejemplo, definiremos la acción abstracta de intercambiar los valores de dos variables. La especificación podría ser:

PROCEDURE Intercambiar( VAR a, b: INTEGER );
           (* a’, b’ = b, a *)

Para escribir esta especificación hemos necesitado distinguir los valores de los argumentos (pasados por referencia) en dos momentos diferentes: al comienzo y al final de la ejecución del procedimiento. Los nombres con prima (‘) representan los valores finales. La expresión:

     a’, b’ = b, a

significa que la pareja de valores de los argumentos a y b, por este orden, al terminar la ejecución del subprograma, coincide con la pareja de valores de b y a, por este orden, al comienzo de la ejecución del subprograma.

Conociendo la especificación podemos ya escribir algún fragmento de programa que utilic este procedimiento. Si queremos ordenar dos valores de menos a mayor, podríamos escribir:

IF p > q THEN
   Intercambiar( p, q )
END

Para escribir este fragmento de programa no hemos necesitado saber cuál es la realización del procedimiento de intercambiar. Por supuesto, para tener un programa completo, que se pueda ejecutar, necesitamos escribir una realización válida. Por ejemplo:

PROCEDURE Intercambiar( VAR a, b: INTEGER );
   VAR x: INTEGER;
BEGIN
   x := a;
   a := b;
   b := x
END Intercambiar;

Al definir procedimientos no podemos limitarnos a usar sólo el paso de argumentos por valor. En programación imperativa las acciones consisten habitualmente en modificar los valores de determinadas variables. Por esta razón se considera normal que los procedimientos usen argumentos pasados por referencia.

De todas maneras conviene seguir una cierta disciplina para que los programas resulten claros y fáciles de entender. Para ello podemos recomendar que los procedimientos se escriban siempre como procedimientos puros, entendiendo por ello que no produzcan efectos laterales o secundarios. Con esto se consigue que la acción que realiza un procedimiento se deduzca en forma inmediata de la invocación de dicha acción. Se garantiza que un procedimiento cumple con esta cualidad si su realización no utiliza:

  • Variables externas al subprograma, a las que se accede directamente por su nombre, de acuerdo con las reglas de visibilidad de bloques.
  • Llamadas a otros subprogramas que no sean procedimientos o funciones puras.

Comparando esta lista de restricciones con la que se estableció para las funciones puras, se observa que hemos suprimido la exigencia de que el procedimiento no lea datos del exterior. En general esta lectura puede considerarse como una asignación de valor, que puede quedar suficientemente bien reflejada en la llamada, si los dispositivos o ficheros de entrada se mencionan explícitamente como argumentos.

De todas maneras es difícil establecer una disciplina precisa con recomendaciones sobre la definición y uso de procedimientos. Hay muchas situaciones en las que la claridad del programa aumenta, de hecho, si se usan procedimientos en que se acceda a variables globales. Así es posible evitar que haya que escribir repetidamente argumentos iguales en cada una de las llamadas al procedimiento. En particular, los procedimientos de lectura del módulo InOut omiten pasar como argumento el fichero de datos de entrada, y asumen por defecto una entrada principal de datos, predefinida.

8.2 Desarrollo por refinamiento usando abstracciones

La metodología de programación estructurada puede ampliarse con la posibilidad de definir operaciones abstractas mediante subprogramas. A continuación se describen dos estrategias de desarrollo diferentes, según qué se escriba primero, si la definición de los subprogramas, o el programa principal que los utiliza.

8.2.1 Desarrollo descendente
La estrategia de desarrollo descendente (en inglés, "Top-Down"), es simplemente el desarrollo por refinamientos sucesivos, teniendo en cuenta además la posibilidad de definir operaciones abstractas. En cada etapa de refinamiento de una operación habrá que optar por una de las alternativas siguientes:

  • Considerar la operación como operación terminal, y CODIFICARLA mediante sentencias del lenguaje de programación.
  • Considerar la operación como operación compleja, y DESCOMPONERLA en otras más sencillas.
  • Considerar la operación como operación abstracta, y ESPECIFICARLA, escribiendo más adelante el subprograma que la realiza.

Para decidir si una operación debe refinarse como operación abstracta habrá que analizar las ventajas que se obtengan, frente a codificación directa o descomposición de la operación en forma de un esquema desarrollado en ese punto del programa.

En general resultará ventajoso refinar una operación como operación abstracta, que se define en forma separada, si se consigue alguna de las ventajas siguientes:

  • Evitar mezclar en un determinado fragmento de programa operaciones con un nivel de detalle muy diferente.
  • Evitar escribir repetidamente fragmentos de código que realicen operaciones análogas.

El beneficio obtenido es, como cabría esperar, una mejora en la claridad del programa. Hay que decir que esto implica un costo ligeramente mayor en términos de eficiencia, ya que siempre se ejecuta más rápidamente una operación si se escriben directamente las sentencias que la realizan, que si se invoca un subprograma que contiene dichas sentencias. La llamada al subprograma representa una acción adicional que consume un cierto tiempo de ejecución.

Por el contrario, hay un aumento de eficiencia en ocupación de memoria si se codifica como subprograma una operación que se invoca varias veces en distintos puntos del programa. En este caso el código de la operación aparece sólo una vez, mientras que si se escribiesen cada vez las sentencias equivalentes el código aparecería repetido varias veces.

8.2.2 Ejemplo: Imprimir la figura de un árbol de navidad
Retomamos aquí el ejemplo desarrollado en el Tema 4. El objetivo es imprimir la silueta del árbol, tal como aparece a continuación:

    *
   ***
  *****
   ***
  *****
 *******
  *****
 *******
*********
    *
    *
    *
  *****

Los primeros pasos de refinamiento eran:

            Imprimir árbol —>
                    Imprimir copa
                    Imprimir tronco
                    Imprimir base

           Imprimir copa —>
                    Imprimir primeras ramas
                    Imprimir segundas ramas
                    Imprimir terceras ramas

Podemos observar la existencia de operaciones análogas, correspondientes a la impresión de los distintos fragmentos. Es relativamente sencillo darse cuenta de que cada una de las "ramas" de la copa del árbol es una figura trapezoidal. Por ejemplo, las "segundas ramas" aparecen dibujadas así:

                   ***
                  *****
                 *******

Esta figura geométrica es un trapecio simétrico. Lo mismo puede decirse de las otras "ramas". Puesto que cada vez se imprime una figura diferente, podremos definir esta acción como parametrizada, dando como argumentos la información necesaria para distinguir cada "rama" particular. Por ejemplo, podemos decidir que el único parámetro necesario es la anchura de la base superior, ya que todas las "ramas" tienen 3 líneas de altura, y cada una de estas líneas añade siempre un asterisco más a cada lado.

La especificación de la impresión de una "rama" como procedimiento se podrá redactar en la forma:

PROCEDURE ImprimirRama( ancho: INTEGER );
 (* Imprimir 3 líneas con ancho, ancho+2 y ancho+4 asteriscos *)

En cuanto a la impresión del tronco y la base, también cabe la posibilidad de considerarlas como operaciones análogas, en ambos casos un rectángulo de asteriscos. Los parámetros serían en este caso la anchura y altura del rectángulo. La especificación sería:

PROCEDURE ImprimirRectángulo( ancho, alto: INTEGER );
 (* Imprimir un rectángulo de ancho x alto asteriscos *)

Con esto se podría escribir ya el programa principal, en el que podemos agrupar la impresión de las ramas en un esquema de bucle.

BEGIN
   (*– Imprimir copa –*)
      FOR rama := 1 TO 5 BY 3 DO
         ImprimirRama( rama )
      END;
   (*– Imprimir tronco –*)
      ImprimirRectangulo( 1, 3 );
   (*– Imprimir base –*)
      ImprimirRectangulo( 5, 1 );
END Arbol.

Ahora falta escribir la realización de las operaciones abstractas especificadas anteriormente. Forzando quizá un poco la idea de buscar operaciones análogas, se puede establecer una relación entre la impresión de las "ramas" y las del tronco o la base. En efecto, un rectángulo puede considerarse como un caso particular de un trapecio. Tanto la operación de ImprimiRama como la de ImprimirRectangulo se pueden apoyar en una operación común de ImprimirTrapecio especificada en la forma siguiente:

PROCEDURE ImprimirTrapecio( ancho, alto, avance: INTEGER );
 (* Imprimir un trapecio de asteriscos, con base superior ‘ancho’, altura ‘alto’, y ‘avance’ asteriscos más a cada lado en cada nueva línea *)

Esta operación la desarrollamos mediante refinamientos:

           Imprimir trapecio —>
              FOR k := 1 TO alto DO
                 Imprimir una línea del trapecio
              END
           Imprimir una línea del trapecio —>
              Imprimir los blancos iniciales
              Imprimir los asteriscos

Para mantener la información del número de asteriscos a cada línea usaremos una variable anchura, que tomará inicialmente el ancho de la línea superior, y se irá incrementando después de imprimir cada línea. Los blancos iniciales se calculan cada vez, fijando como parámetro constante la posición del centro de línea.

Al desarrollar este procedimiento se observa una analogía entre la operación de escribir los espacios en blanco y la de escribir los asteriscos. Ambas operaciones se refinan como la operación abstracta de imprimir un mismo carácter un cierto número de veces. Para ello se especifica el procedimiento:

PROCEDURE ImprimirN ( c: CHAR; N: INTEGER );
 (* Imprimir ‘N’ veces seguidas el carácter ‘c’ *)

El programa completo, incluyendo todos los procedimientos, es el siguiente:

(****************************************************************************************************
*
*    Programa: ARBOL
*
*    Descripción:
*      Este programa imprime la silueta de un árbol de Navidad, hecha con asteriscos.
*
****************************************************************************************************)
MODULE Arbol;

(*===================================================================================================
    IMPORTACIÓN Y DECLARACIONES DEL PROGRAMA
===================================================================================================*)
  FROM InOut IMPORT Write, WriteLn;
  CONST centro = 20;            (* centro de cada línea *)
  VAR rama: INTEGER;            (* contador de ramas *)

  PROCEDURE ImprimirN( c: CHAR; N: INTEGER );
  (* Imprimir ‘N’ veces seguidas el carácter ‘c’ *)
    VAR k: INTEGER;
  BEGIN
    FOR k:= 1 TO N DO
      Write( c )
    END
  END ImprimirN;

  PROCEDURE ImprimirTrapecio( ancho, alto, avance: INTEGER );
  (* Imprimir un trapecio de asteriscos, con base superior ‘ancho’,
       altura ‘alto’, y ‘avance’ asteriscos más a cada lado en
       cada nueva línea                                            *)
    VAR k: INTEGER;            (* contador de líneas *)
        anchura: INTEGER;        (* número de asteriscos *)

  BEGIN
    anchura := ancho;
    FOR k := 1 TO alto DO
      ImprimirN( ‘ ‘, centro – anchura DIV 2);
      ImprimirN( ‘*’, anchura );
      WriteLn;
      INC( anchura, avance*2)
    END
  END ImprimirTrapecio;

  PROCEDURE ImprimirRama( ancho: INTEGER );
  (* Imprimir 3 líneas con ancho, ancho+2, y ancho+4 asteriscos *)
  BEGIN
    ImprimirTrapecio( ancho, 3, 1)
  END ImprimirRama;

  PROCEDURE ImprimirRectangulo( ancho, alto: INTEGER );
  (* Imprimir un rectángulo de ancho x alto asteriscos *)
  BEGIN
    ImprimirTrapecio( ancho, alto, 0)
  END ImprimirRectangulo;

BEGIN

(*======================================================================================================
    PARTE EJECUTABLE DEL PROGRAMA
======================================================================================================*) 
  (*– Imprimir copa –*)
    FOR rama := 1 TO 5 BY 2 DO
      ImprimirRama( rama )
    END;
  (*– Imprimir tronco –*)
    ImprimirRectangulo( 1, 3 );
  (*– Imprimir base –*)
    ImprimirRectangulo( 5, 1 );
END Arbol.

Comparando esta redacción del programa con la que se había desarrollado en el Tema 4, se observa que el programa resulta ahora más largo, aunque cada parte separada del programa es más sencilla. En la versión anterior la parte ejecutable del programa principal era más compleja que ahora.

Con esta nueva redacción se obtiene una ventaja adicional, que se ha producido como efecto de la labor de abstracción realizada para especificar los subprogramas. Operaciones que antes se consideraban por separado, ahora se han refundido en una sola operación abstracta y parametrizada. La parametrización tiene la ventaja de que se facilita la modificación posterior del programa.

En efecto, si quisiéramos cambiar el programa para imprimir una figura de árbol algo diferente, en la versión inicial habría sido necesario cambiar casi toda la parte ejecutable del programa, sentencia por sentencia. Ahora la mayor parte del programa está constituida por las definiciones de las operaciones abstractas, que se pueden mantener sin cambios, y sólo hay que rectificar la parte del código del programa principal, que es relativamente corta.

Por ejemplo, podemos modificar el código del programa principal para imprimir un árbol más grande, tal como se indica en el programa ArbolGrande, donde los cambios se han destacado en letra negra.

Con la version inicial del programa habríamos tenido que escribir de nuevo al menos unas 13 líneas del programa. Ahora no ha sido necesaria ninguna línea nueva y tan sólo hemos tenido que retocar 3.

MODULE ArbolGrande;
. . . .
<< Definición de procedimientos >>
. . . .

BEGIN
   (*– Imprimir copa –*)
      FOR rama := 1 TO 9 BY 2 DO
         ImprimirRama( rama )
      END;
   (*– Imprimir tronco –*)
      ImprimirRectangulo( 3, 5 );
   (*– Imprimir base –*)
      ImprimirRectangulo( 9, 2 );
END ArbolGrande;

El resultado de la ejecución del programa ArbolGrande es el siguiente:

      *
     ***
    *****
     ***
    *****
   *******
    *****
   *******
  *********
   *******
  *********
 ***********
  *********
 ***********
*************
     ***
     ***
     ***
     ***
     ***
  *********
  *********

8.2.3 Ejemplo: Imprimir una tabla de números primos
En el ejemplo anterior se buscó de manera insistente la analogía entre operaciones y su especificación como operaciones parametrizadas. En este ejemplo se atenderá fundamentalmente a la limitación en el nivel de detalle.

El objetivo de este programa de ejemplo es imprimir una tabla con los números primos hasta un límite dado, formando varias columnas de números a lo ancho del listado. Si decidimos imprimir los números primos hasta 100, a cuaro columnas de 10 caracteres de ancho cada una, el resultado deberá ser el que aparece a continuación:

 1         2         3         5
 7        11        13        17
19        23        29        31
37        41        43        47
53        59        61        67
71        73        79        83

Los primeros pasos de refinamiento serán:

         Imprimir la tabla de números primos de 1 a n —>
            FOR k := 1 TO N DO
               Imprimir k, si es primo
            END

         Imprimir k, si es primo —>
            IF k es primo THEN
               Imprimir k, tabulando
            END

Ahora decidimos limitar el nivel de detalle, y definir como operaciones abstractas las que faltan por refinar. Sus especificaciones serían:

PROCEDURE EsPrimo( k: INTEGER ): BOOLEAN;
 (* Indica si ‘k’ es un número primo *)

PROCEDURE ImprimirTabulando( k: INTEGER );
 (* Imprimir ‘k’ tabulando a 4 columnas de 10 caracteres *)

A continuación podemos desarrollar la realización de estos subprogramas. La función que determina si un número es primo puede realizarse sencillamente probando todos los divisores posibles. Esta realización es poco eficiente, pero muy sencilla de programar.

PROCEDURE EsPrimo( k: INTEGER ); BOOLEAN;
(* Indica si ‘k’ es un número primo *)
   VAR d: INTEGER;     (* posible divisor *)
BEGIN
   FOR d := 2 TO k-1 DO  (* es igual de válido utilizar (k DIV 2) como cota superior *)
      IF k MOD d = 0 THEN
         RETURN FALSE
      END
   END;
   RETURN TRUE
END EsPrimo;

Para desarrollar la realización del procedimiento de imprimir tabulando hay que analizar algunas cuestiones previas. La especificación se ha establecido pasando como argumento solamente el número que hay que imprimir, reflejando de esta manera la forma natural en que se ha descrito esta acción abstracta. Sin embargo esta información es insuficiente para realizar la acción, ya que es necesario saber qué columna toca imprimir para poder decidir si hay que saltar de línea o no.

En este ejemplo se decide usar una variable global columna para mantener dicha información. La variable contendrá en cada momento el número de la columna en que aparecería escrito el próximo número si previamente no se saltase de línea.

El refinamiento de esta operación será el siguiente:

         Imprimir k, tabulando —>
            Saltar de línea, si es necesario
            Imprimir k y actualizar la columna

         Saltar de línea, si es necesario —>
             IF columna > 4 THEN
                WriteLn;
                columna := 1
             END

El programa completo, incluyendo la definición de todos los subprogramas necesarios, es el siguiente:

(****************************************************************************************************
*
*    Programa: PRIMOS
*
*    Descripción:
*      Este programa imprime una tabla de números primos, tabulando a cuatro columnas.
****************************************************************************************************)
MODULE Primos;

(*===================================================================================================
    IMPORTACIÓN Y DECLARACIONES DE PROGRAMA
===================================================================================================*)
  FROM InOut IMPORT WriteInt, WriteLn;

  CONST N = 100;                (* rango de números *)
  VAR columna: INTEGER;                (* columna a imprimir *)
  VAR K: INTEGER;                (* número a ensayar *)

  PROCEDURE EsPrimo( k: INTEGER ): BOOLEAN;
  (* Indica si ‘k’ es un número primo *)
    VAR d: INTEGER;            (* posible divisor *)
  BEGIN
    FOR d := 2 TO k-1 DO
      IF k MOD d = 0 THEN
        RETURN FALSE
      END
    END;
    RETURN TRUE
  END EsPrimo;

  PROCEDURE ImprimirTabulando( k: INTEGER );
  (* Imprimir ‘k’ tabulando a 4 columnas de 10 caracteres *)
  BEGIN
    IF columna > 4 THEN
      columna := 1;
      WriteLn
    END;
    WriteInt( k, 10 );
    INC( columna )
  END ImprimirTabulando;

BEGIN

(*=====================================================================================================
    PARTE EJECUTABLE DEL PROGRAMA
=====================================================================================================*)
  columna := 1;
  FOR K := 1 TO N DO
    IF EsPrimo( K ) THEN
      ImprimirTabulando( K )
    END
  END;
  WriteLn
END Primos.

8.2.4 Reutilización
La realización de ciertas operaciones como subprogramas independientes facilita lo que se llama reutilización de software. Si la operación identificada como operación abstracta tiene un cierto sentido en sí misma, es muy posible que resulte de utilidad en otros programas, además de en aquél para el cual se ha desarrollado. La escritura de otros programas que utilicen esa misma operación resulta más sencilla, ya que se aprovecha el código de su definición, que ya estaba escrito.

Aplicaremos esta idea a los subprogramas desarrollados para imprimir el árbol de Navidad. Las operaciones abstractas definidas allí permiten imprimir con bloques de asteriscos figuras trapezoidales, o simplemente rectangulares, de dimensiones variables. Cualquier figura que pueda descomponerse en secciones de estas formas se podrá imprimir fácilmente usando los procedimientos ya definidos.

Por ejemplo, si queremos imprimir la figura de una casa de juguete, tal como la siguiente:

     **
     **
  *********
 ***********
*************
  *********
  *********
  *********

sólo tendremos que escribir un fragmento de programa así:

MODULE Casa;
   . . . .
BEGIN
   ImprimirRectangulo( 2, 2 );
   ImprimirRama( 9 );
   ImprimirRectangulo( 9, 3 );
END Casa.

y copiar en la parte de declaraciones las definiciones de los procedimientos ya desarrollados en el programa árbol de Navidad.

A continuación se presentan más ejemplos, que aprovechan subprogramas desarrollados de antemano.

Fundamentos de programación con Módula-2 (17)

7.5 Visibilidad. Estructura de bloques

Los argumentos no son la única forma de comunicación entre las sentencias de un subprograma y el programa que lo invoca. Existe también la posibilidad de que desde dentro del subprograma se puedan "ver" directamente ciertos elementos del programa que lo usa.

Un subprograma se define de la misma forma que un programa completo, y puede contener, en su definición, declaración de constantes, variables y otros subprogramas. Según se muestra en la Figura 7.2, todos estos elementos de la definición constituyen un bloque. Dichos elementos tienen un sentido local, de manera que no son accesibles desde el exterior del bloque. Es decir, desde el bloque en el que se declara el subprogrma no se tiene acceso al bloque interior de dicho subprograma (recuadros  trazos de la Figura 7.2).

Figura(7_2)
     Figura 7.2: Estructura de bloques.

Por el contrario, los elementos definidos en el programa principal pueden ser usados por cualquier subprograma definido dentro de él. Esto es, dentro de un recuadro a trazos de la Figura 7.2 se puede hacer uso de los elementos definidos fuera de él. Así, por ejemplo, el procedimiento EscribirResultado definido anteriormente, utiliza la variable global del programa resultado.

La utilización de las variables globales es otra manera en que un subprograma puede producir resultados, asignando valores directamente a cualquiera de las variables del programa principal que lo utiliza.

Por ejemplo, el procedimiento que se define a continuación para calcular el perímetro, deja su resultado en la variable global perimetro. Por tanto, se produce como resultado de la ejecución, la modificación de una variable aunque no está indicada en su interfaz.

PROCEDURE CalcularPerimetro;
   VAR
      ladoAB, ladoAC, ladoBC : REAL;
. . . . .
(* Definición de la función distancia *)
. . . . .
BEGIN
   ladoAB := Distancia(xA, yA, xB, yB);
   ladoAC := Distancia(xA, yA, xC, yC);
   ladoBC := Distancia(xB, yB, xC, yC);
   perimetro := ladoAB + ladoAC + ladoBC;
END CalcularPerimetro;

Figura(7_3) 
     Figura 7.3: Acceso a elementos de distintos bloques.

En la Figura 7.3 se muestra una estructura general de programa con diversos bloques anidados. Las reglas de visibilidad entre bloques establecen como posibilidades de acceso a los distintos elementos del programa las indicadas por las flechas. Esto es, cualquier bloque tiene visibilidad hacia los bloques exteriores, pero nunca hacia los interiores a él mismo. Para la situación mostrada en la Figura 7.3 se puede establecer la siguiente tabla de accesos:

                Bloque                   Puede acceder a
                   A                               A
                   B                              B, A
                   C                              C, B, A
                   D                              D, C, B, A
                   E                              E, A
                   F                              F, E, A
                   G                             G, E, A


7.6 Problemas de uso

El empleo de funciones y procedimientos utilizando cualquiera de las dos formas de paso de argumentos, las reglas de visibilidad entre ellos, etc., ofrecen grandes posibilidades para el desarrollo de los programas. Sin embargo, un uso incorrecto de estas posibilidades puede dar lugar a ciertos problemas. En este apartado se analizan algunos de estos problemas y se dan las directrices para poderlos evitar.

7.6.1 Redefinición de elementos
Dentro de cada bloque se pueden definir sus elementos dándoles el nombre que se considere mas adecuado en cada caso. Incluso es posible dar el mismo nombre a elementos definidos en distintos bloques. Por ejemplo, es habitual utilizar las variables de nombre i, j, k, etc. para los índices de las sentencias FOR. Si estas redefiniciones se realizan en bloques no anidados entre ellos, tales como los B y E o los F y G de la Figura 7.3, no existe ningún problema pues en cada uno de ellos estas variables tienen un carácter local que no afecta a las definiciones de los otros bloques.

Cuando las redefiniciones se realizan en bloques que están anidados, tales como los A, B, C y D o los A, E y F de la Figura 7.3, la situación es distinta. De acuerdo con las reglas de visibilidad, el bloque más interno tiene acceso a todos los elementos definidos en cualquiera de los bloques más externos. Al dar un nombre ya utilizado a un nuevo elemento en el bloque interno, automáticamente se pierde la posibilidad de acceso al elemento del mismo nombre existente en cualquiera de los bloques más externos. Por ejemplo:

PROCEDURE Principal ( VAR Exterior, Interior : INTEGER );
   VAR
      superficie : INTEGER;
  
   PROCEDURE Secundario;

      PROCEDURE Interior;
         CONST Exterior = 30;
      BEGIN
         superficie := Exterior * Exterior;
      END Interior;

   BEGIN
      Interior;
      Exterior := superficie DIV 2;
   END Secundario;

BEGIN
   Secundario;
   Interior := Exterior – 5;
END Principal;

el cálculo de superficie en el procedimiento Interior utiliza la constante Exterior con valor igual a 30. El procedimiento Secundario utiliza su procedimiento Interior y el argumento Exterior del procedimiento Principal. Solamente dentro del procedimiento Principal se tiene acceso al argumento Interior del mismo.

Evidentemente, el empleo de elementos diferentes con el mismo nombre aumenta la complejidad del programa y se dificulta mucho su comprensión. Por otro lado, se abre una vía de errores no detectables en compilación. Aunque se pretenda utilizar un símbolo como local, si se olvida su redefinición, se asumirá incorrectamente el significado dado como símbolo externo. Por tanto, salvo que sea imprescindible no se debe utilizar la redefinición de elementos.

7.6.2 Efectos secundarios.
Cuando un subprograma modifica alguna variable externa, se dice que está produciendo efectos secundarios o laterales (en inglés side effects). El uso de subprogramas con efectos secundarios debe hacerse con precaución.

Al definir la interfaz de un subprograma, con su declaración se está asumiendo un compromiso. Este compromiso debería indicar sucales son los únicos elementos que se pueden modificar desde dentro del subprograma. De esta forma, para utilizar el subprograma solo se necesita conocer su interfaz y a partir de ella establecer los posibles resultados que se pueden producir. Cuando un subprograma produce efectos secundarios, está incumpliento el compromiso establecido en su interfaz. Esto dificulta la comprensión del subprograma y puede producir resultados inesperados.

Solo si el número de argumentos necesarios en un subprograma es muy grande o en situaciones que requieran el paso en los argumentos de grandes cantidades de datos está justificado la utilización de efectos secundarios. En cualquier caso, si un subprograma produce efectos secundarios, estos deben indicarse explícitamente en los comentarios de su cabecera.

Se entiende por funcionalidad la cualidad que poseen aquellos subprogramas que permiten asegurar que siempre que se llamen con los mismos argumentos producirán exactamente los mismos resultados. Esto se logra evitando la utilización de efectos secundarios. Esta cualidad también se denomina transparencia referencial.

La cualidad de funcionalidad es deseable tanto para las funciones como para los procedimientos. Sin embargo, para las funciones es una cualidad casi imprescindible. Resulta muy difícil de justificar y comprender que se produzcan efectos secundarios al utilizar una función dentro de una expresión. Por ejemplo, no tiene sentido que como resultado de la llamada a la función Distacia se modifique el valor de perimetro. Idealmente, además todos los argumentos de las funciones deberían pasarse por valor, dado que el único resultado deseable de la función es el declarado en su interfaz.

En general, siempre es posible conseguir que un subprograma no tenga efectos secundarios. Por ejemplo, el procedimiento Interior del apartado anterior modifica la variable superficie, sin que se le pase como argumento. Este procedimiento se puede reescribir modificando su interfaz para que dicha variable se tenga que pasar como argumento y no se produzcan efectos secundarios:

PROCEDURE Interior( VAR s0 : INTEGER );
   CONST Exterior = 30;
BEGIN
   s0 := Exterior * Exterior;
END Interior;

7.6.3 Doble referencia
Se produce doble referencia (en inglés aliasing ) cuando un mismo elemento se referencia con dos nombres distintos. Fundamentalmente, esto puede ocurrir en dos situaciones muy concretas.

1.- Cuando un subprograma utiliza una variable externa que también se le pasa como argumento.
2.- Cuando para utilizar un subprograma se pasa la misma variable en dos o más argumentos.

Habitualmente, un subprograma se escribe pensando que todos sus argumentos son distintos y que nunca coincidirán con ninguna variable externa ya utilizada dentro de él. La doble referencia produce resultados incomprensibles a primera vista. Consideremos, por ejemplo, el siguiente fragmento de programa:

. . . . .
   VAR global : INTEGER;
. . . . .
   PROCEDURE Cuadrado( VAR dato : INTEGER );
   BEGIN
      global := 5; dato := dato * dato;
   END Cuadrado;
. . . . .
global := 3;
Cuadrado( global );
. . . . .

Después de la ejecución del procedimiento Cuadrado( global ) la variable global tiene un valor igual a 25. Esto es debido a que el procedimiento Cuadrado utiliza directamente como dato la variable global pasada por referencia. En el momento del cálculo del producto, el valor de dicha variable es 5 y por tanto el producto por sí misma es 25.

Una situación similar se puede producir con el siguiente procedimiento:

PROCEDURE CuadradoCubo( VAR x, x2, x3 : INTEGER );
BEGIN
   x2 := x*x; x3 := x*x*x;
END CuadradoCubo;

Si se utiliza con los siguientes argumentos y valores:

. . . .
A := 4;
CuadradoCubo(A, A, B);
. . . .

después de la ejecución de este fragmento, los valores de las variables son A igual a 16 y B igual a 4096.

Como conclusión se puede decir que no se debe utilizar la doble referencia, salvo que el subprograma se diseñe pensando en esa posibilidad. Esto útlimo deberá quedar claro en los comentarios del subprograma.

7.7 Ejemplos de programas

Para finalizar este tema se muestran tres programas completos y sus correspondientes resultados. Estos programas utilizan algunas de las funciones y procedimientos que han sido desarrollados a lo largo de todo este tema.

7.7.1 Ejemplo: Raíces de una ecuación de segundo grado
Con este programa se trata de calcular las raíces de una ecuación de segundo grado de la forma:

ax^2 + bx + c = 0

las raices pueden ser reales o imaginarias. Los coeficientes a, b y c serán reales y se leerán de teclado. El programa tendrá en cuenta los siguientes casos:

  • Si a, b y c son iguales a cero: Se considerará una ecuación no válida.
  • Si a y b son iguales a cero: La solución es imposible.
  • Si a es igual a cero: Una única ráiz.
  • Si a, b y c son distintos de cero: Dos raíces reales o imaginarias.

en este último caso, el cálculo de las raíces se realiza mediante la fórmula:
Ejemplo_raices

Para la lectura de los coeficientes conviene utilizar un procedimiento, y así se pueden leer los 3 coeficientes de igual manera. En el programa Raices está recogido el listado completo.

(******************************************************************************
*
*    Programa: Raices
*
*    Descripción:
*      Este programa calcula las raices de una ecuación de segundo grado:
*        ax^2 + bx + c
*
******************************************************************************)
MODULE Raices;

(*=============================================================================
    IMPORTACIÓN Y DECLARACIONES DEL PROGRAMA
=============================================================================*)
  FROM InOut IMPORT
    WriteString, WriteLn, WriteInt;
  FROM RealInOut IMPORT
    ReadReal, WriteReal;
  FROM MathLib0 IMPORT sqrt;

  VAR
    valorA, valorB, valorC,         (* Coeficientes de la ecuación *)
    parteUno, parteDos,            (* Variables intermedias de calculo *)
    valorD : REAL;            (* Discriminante de la ecuación *)

PROCEDURE LeerValor(grado : INTEGER; VAR valor : REAL);
(*======================================================
   Procedimiento para leer un coeficiente
======================================================*)
BEGIN
  WriteString("¿Coeficiente de");
  WriteInt(grado, 2); WriteString("º grado ?");
  ReadReal(valor); WriteLn;
END LeerValor;

PROCEDURE Discriminante(a, b, c : REAL) : REAL;
(*======================================================
   Función para calcular el discriminante   
======================================================*)
BEGIN
  RETURN b*b – 4.0*a*c;
END Discriminante;

BEGIN

(*==============================================================================
    PARTE EJECUTABLE DEL PROGRAMA
==============================================================================*)
  LeerValor(2, valorA);
  LeerValor(1, valorB);
  LeerValor(0, valorC);
  IF valorA = 0.0 THEN
    IF valorB = 0.0 THEN
      IF valorC = 0.0 THEN
        WriteLn; WriteString("Ecuación No Valida")
      ELSE
          WriteLn; WriteString("Solución Imposible")
      END
    ELSE
      WriteLn; WriteString("Raiz Unica = ");
      WriteReal(- valorC/valorB, 10)
    END
  ELSE
    valorD := Discriminante(valorA, valorB, valorC);
    parteUno := -valorB/(2.0*valorA);
    parteDos := sqrt(ABS(valorD))/(2.0*valorA);
    IF valorD > 0.0 THEN
      WriteLn; WriteString("Raices Reales : "); WriteLn;
      WriteReal(parteUno + parteDos, 10); WriteString(" y ");
      WriteLn; WriteReal(parteUno – parteDos, 10);
    ELSE
      WriteLn; WriteString( "Raices Complejas :"); WriteLn;
      WriteString("Parte Real = ");
      WriteReal(parteUno, 10); WriteLn;
      WriteString(" y Parte Imaginaria = ");
      WriteReal(parteDos,10)
    END
  END
END Raices.

El resultado obtenido por el programa para la ecuación x^2 + 2x + 2 = 0 es el siguiente:

¿Coeficiente de 2º grado ?1.0
¿Coeficiente de 1º grado ?2.0
¿Coeficiente de 0º grado ?2.0

Raices Complejas :
Parte Real = -1.000E+00
 y Parte Imaginaria =  1.000E+00

7.7.2 Ejemplo: Ordenar tres valores
Este programa s una versión mejorada del mostrado en el tema 5. En este caso se utiliza el procedimiento para la ordenación de dos datos, que fue desarrollado en el apartado 7.4 de este tema. Además, se utiliza un procedimiento para leer uno a uno los tres datos a ordenadar. El programa permanece en un bucle hasta que se indica que no se necesitan ordenar más datos.

A continuación se recoge el listado completo.

(*************************************************************************************
*
*    Programa: Orden3
*
*    Descripción:
*      Este programa ordena 3 valores
*
*************************************************************************************)
MODULE Orden3;

(*====================================================================================
    IMPORTACIÓN Y DECLARACIONES DEL PROGRAMA
====================================================================================*)
  FROM InOut IMPORT
    WriteString, WriteLn, WriteInt,
    Read, Write, ReadInt;

  VAR
    valorUno, valorDos, valorTres : INTEGER;
    tecla : CHAR;

PROCEDURE LeerDato( l : INTEGER; VAR Dato : INTEGER );
(*===================================================
Procedimiento para leer un dato
===================================================*)
BEGIN
  WriteInt(l, 3); WriteString(" ¿ º Dato ?");
  ReadInt(Dato);
  WriteLn;
END LeerDato;

PROCEDURE OrdenarDos(VAR y,z : INTEGER);
(*==================================================
Procedimiento para ordenar dos datos
==================================================*)
  VAR
    aux : INTEGER;
BEGIN
  IF y > z THEN aux := y; y := z; z := aux END
END OrdenarDos;

BEGIN

(*=====================================================================================
    PARTE EJECUTABLE DEL PROGRAMA
=====================================================================================*)
  tecla := "S";
  WHILE tecla <> "N" DO
    (*– Leer los datos –*)
      LeerDato(1, valorUno);
      LeerDato(2, valorDos);
      LeerDato(3, valorTres);
    (*– Ordenar los datos –*)
      OrdenarDos(valorUno, valorDos);
      OrdenarDos(valorUno, valorTres);
      OrdenarDos(valorDos, valorTres);
    (*– Escribir resultados –*)
      WriteLn; WriteString("Datos Ordenados = ");
      WriteInt(valorUno,5); WriteInt(valorDos,5);
      WriteInt(valorTres,5); WriteLn;
    (*– Comprobar si se continua –*)
      WriteString("¿Desea Continuar (S/N)? ");
      Read(tecla); Write(tecla); WriteLn
  END
END Orden3.

Los resultados obtenidos en dos ordenaciones consecutivas son los siguientes:

  1 ¿ º Dato ?12
  2 ¿ º Dato ?3
  3 ¿ º Dato ?89

Datos Ordenados =     3   12   89
¿Desea Continuar (S/N)? s
  1 ¿ º Dato ?9
  2 ¿ º Dato ?34
  3 ¿ º Dato ?2

Datos Ordenados =     2    9   34
¿Desea Contuniar (S/N)? N

7.7.3 Ejemplo: Perímetro de un triángulo
Este programa ha sido desarrollado casi completamente a lo largo de este tema. En este apartado se trata solamente de mostrar su estructura global, en la que se aprecian los niveles de anidamiento entre los distintos procedimientos y funciones y sus dependencias. A continuación se recoge el listado completo.

(****************************************************************************************************
*
*    Programa: Perimetro
*
*    Descripción:
*      Programa para calcular el perímetro rodeado por los tres vértices de un triángulo.
*
*
****************************************************************************************************)
MODULE Perimetro;

(*===================================================================================================
    IMPORTACIÓN Y DECLARACIONES DEL PROGRAMA
===================================================================================================*)
  FROM InOut IMPORT
    Write, WriteString, WriteLn;
  FROM RealInOut IMPORT
    WriteReal, ReadReal;
  FROM MathLib0 IMPORT sqrt;
  VAR xA, yA, xB, yB, xC, yC, (* Coordenadas de los puntos *)
    perimetro : REAL;        (* Valor del perimetro *)

PROCEDURE LeerVertices;
(*================================================================
Procedimiento para leer las coordenadas de los 3 vértices
================================================================*)

  PROCEDURE LeerCoordenadas(Vertice: CHAR; VAR x, y : REAL);
  (*================================================================
  Procedimiento para leer las coordenadas X e Y de un punto.
  Para facilitar la identificación del punto, se tiene que
  pasar la letra que lo identifica como argumento
  ================================================================*)
  BEGIN
    WriteString("Punto "); Write(Vertice); WriteLn;
    WriteString("¿Coordenada X?");
    ReadReal(x); WriteLn;
    WriteString("¿Coordenada Y?");
    ReadReal(y); WriteLn;
  END LeerCoordenadas;
BEGIN
  LeerCoordenadas("A", xA, yA);
  LeerCoordenadas("B", xB, yB);
  LeerCoordenadas("C", xC, yC);
END LeerVertices;

PROCEDURE CalcularPerimetro;
(*==================================================================
Procedimiento para calcular el perímetro rodeado por tres puntos
==================================================================*)
  VAR
    ladoAB, ladoAc, ladoBC : REAL;
  PROCEDURE Distancia(x1, y1, x2, y2 : REAL): REAL;
  (*================================================================
  Función para calcular la distancia que hay entre dos puntos
  (x1,y1) y (x2,y2)
  ================================================================*)
    VAR
      incrX, incrY : REAL;
  BEGIN
    incrX := x2 – x1; incrY := y2 – y1;
    RETURN sqrt(incrX*incrX + incrY*incrY);
  END Distancia;
BEGIN
  ladoAB := Distancia(xA, yA, xB, yB);
  ladoAc := Distancia(xA, yA, xC, yC);
  ladoBC := Distancia(xB, yB, xC, yC);
  perimetro := ladoAB + ladoAc + ladoBC;
END CalcularPerimetro;

PROCEDURE ImprimirPerimetro;
(*==================================================================
Procedimiento para imprimir la variable globar perimetro
==================================================================*)
BEGIN
  WriteLn;
  WriteString("El perímetro es igual a ");
  WriteReal(perimetro,3); WriteLn;
END ImprimirPerimetro;

BEGIN

(*==================================================================
    PARTE EJECUTABLE DEL PROGRAMA   
==================================================================*)
  LeerVertices;
  CalcularPerimetro;
  ImprimirPerimetro;
END Perimetro.

El resultado de su ejecución es el siguiente:

Punto A
¿Coordenada X?3.0
¿Coordenada Y?0.0
Punto B
¿Coordenada X?0.0
¿Coordenada Y?0.0
Punto C
¿Coordenada X?0.0
¿Coordenada Y?4.0

El perímetro es igual a  1.20E+01