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

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