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.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s