LAS ABEJAS

Una tarde de sol, dos chicos se encontraban en un jardín florido, observando a las abejas que zumbaban alrededor. Ambos eran muy aficionados a la observación de insectos, y cada uno de ellos escribió un informe acerca de sus observaciones.
Según el primer informe, concluyeron que catorce de las abejas eran amarillas y el resto marrones. Doce de las abejas eran machos. Trece de las abejas eran grandes y las demás pequeñas. Cuatro de las amarillas eran grandes, cinco de las amarillas eran machos, y tres de los machos eran grandes. Había solamente una abeja macho, amarilla y grande, y todas las abejas eran o bien grandes, o machos o marillas. El segundo informe era bastante diferente. Según esta reseña, la mitad de las abejas se sentían atraídas por el trébol, un cuarto de ellas por las flores del diente de león, un séptimo de las abejas parecía preferir los jacintos, mientras que las tres abejas restantes giraban en el aire sin poder decidirse, aparentemente, en cuál de las flores se posarían.

¿Debería dudarse de uno u otro de estos informes? Si así fuese, ¿de cuál de ellos? ¿Concuerdan los informes?

::SOLUCIÓN::
aqui

CUADRADOS SIN REPETIR CIFRA

Los siguientes cuadrados tienen todas sus cifras diferentes:

13^2 = 169
36^2 = 1296
286^2 = 81796
322^2 = 103684
1027^2 = 1054729
6901^2 = 47623801
10124^2 = 102495376
32043^2 = 1026753849

¿Podría encontrar usted alguno más?

:: SOLUCIÓN ::
aquí

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

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

UNIDAD DIDÁCTICA II

 

Tema 7
Funciones y Procedimientos

El concepto de subprograma es fundamental para poder desarrollar programas grandes. Este tema y el siguiente se dedican por entero a introducir dicho concepto.

Las dos formas clásicas de subprogramas, disponibles prácticamente en cualquier lenguaje imperativo, son las funciones y los procedimientos. En este tema se detalla cómo se definen y utilizan ambos tipos de subprogramas, y las diferencias que existen entre ellos.

El tema se completa haciendo explícitas las principales dificultades de uso que conlleva el empleo de subprogramas para los programadores principiantes, y la necesidad de una adecuada disciplina de programación para soslayarlos.

7.1 Concepto de subprograma

Un subprograma, como su nombre indica, es una parte de un programa. Como mecanismo de programació, un subprograma es una parte de un programa que se desarrolla por separado y se utiliza invocándolo mediante un nombre simbólico.

Desde el punto de vista de una buena metodología de programación, el mecanismo de subprograma debe utilizarse para fragmentos del programa que tengan un cierto sentido en sí mismos. Si se hace así, podríamos decir que, al igual que un programa sirve para resolver un problema, un subprograma sirve para resolver un subproblema.

El empleo de subprogramas, desarrollando por separado ciertas partes del programa, resulta especialmente ventajoso en los casos siguientes:

  • En programas complejos: Si el programa se escribe todo seguido resulta muy complicado de entender, porque se difumina la visión de su estructura global entre la gran cantidad de operaciones que forman el código del programa (!los árboles impiden ver el bosque!). Aislando ciertas partes como subprogramas separados se reduce la complejidad de la visión global del programa.
  • Cuando se repiten operaciones análogas: Definiendo esa operación como subprograma separado, su código se escribirá sólo una vez, aunque luego se use en muchos puntos del programa. El tamaño total del programa será menor que si se escribiera el código completo de la operación cada vez que se necesita.

La técnica de refinamientos sucesivos sugiere descomponer las operaciones complejas de un programa en otras más simples. En sucesivos pasos de refinamiento, cada operación se vuelve a descomponer hasta que todo el programa se puede escribir utilizando las sentencias disponibles en el lenguaje empleado.

Hasta el momento hemos continuado los refinamientos hasta llegar a las sentencias básicas de Modula-2. Podemos ver ahora sobre un ejemplo cómo es el programa resultante si las operaciones intermedias se definen como subprogramas en Modula-2.

Por ejemplo, consideremos un programa para calcular el perímetro del triángulo formado por tres puntos (A, B y C), según se muestra en la Figura 7.1.

Figura(7_1)  
     Figura 7.1: Perímetro de un triángulo

Los primeros pasos del refinamiento serían los siguientes:

                Calcular e imprimir el perímetro —>
                        Leer las coordenadas de los vértices
                        Calcular el perímetro
                        Imprimir el perímetro

A su vez, la operación de lectura de los puntos se puede descomponer en una secuencia de lecturas de las coordenadas de cada uno de los tres puntos:

                 Leer las coordenadas de los vértices —>
                       Leer coordenadas del punto A
                       Leer coordenadas del punto B
                       Leer coordenadas del punto C

Definiendo cada subproblema somo subprograma, el aspecto del programa, en forma esquemática, sería el siguiente:

MODULE Perimetro;
   . . . .
   PROCEDURE LeerVertices;
   . . . .
   BEGIN
      LeerCoordenadas( A );
      LeerCoordenadas( B );
      LeerCoordenadas( C );
   END LeerVertices;
   . . . .
BEGIN
   LeerVertices;
   CalcularPerimetro;
   ImprimirPerimetro
END Perimetro.

En este ejemplo vemos cómo el subprograma de leer las coordenadas de los vértices aparece descrito en la parte declarativa del programa, y luego se usa, invocándolo por su nombre, en la parte ejecutiva.

A continuación se estudian las dos formas fundamentales de subprogramas en programación imperativa: Funciones y Procedimientos, y su manejo utilizando el lenguaje Modula-2.

7.2 Funciones

Cuando se diseña y desarrolla un programa aparecen con frecuencia operaciones significativas que dan como resultado un valor simple y único en función de ciertos parámetros. Por ejemplo:

    Potencia:                            x^n
    Volumen de un cubo:           Lado^3
    Área de un triángulo:            (Base * Altura) / 2
    Distancia entre dos puntos:  ((x1 – x2)^2 + (y1 – y2)^2)^1/2

Estas operaciones se pueden considerar subprogramas y más exactamente funciones. Una función es un tipo de subprograma que calcula como resultado un valor simple y único a partir de otros valores dados como argumentos. En líneas generales, una función se asemeja bastante a la idea matemática de función F(x, y, …), con argumentos x, y, …

7.2.1 Definición de funciones
El primer paso en el manejo de una función es declarar su interfaz. Esta declaración incluye su nombre, los argumentos que necesita con el correspondiente tipo para cada uno de ellos, y el tipo de resultado que proporciona. En Modula-2 esto se realiza escribiendo una cabecera de función de la siguiente forma:

            PROCEDURE Nombre( argumento: Tipo; …) : TipoResultado

La declaración se inicia con la palabra clave PROCEDURE y a continuación el nombre de la función, que debe ser un identificador. Seguidamente, la lista de los argumentos, entre paréntesis, y separados por el carácter de punto y coma (;). Las declaraciones de argumentos son similares a las declaraciones de variables: por cada argumento se declaran su nombre y su tipo, separados por coma (,), y poniendo el tipo sólo una vez al final. Finalmente, se declara el tipo del resultado a continuación del carácter dos puntos ( : ).

Es frecuente que los lenguajes de programación utilicen la palabra PROCEDURE para designar procedimientos, y la palabra FUNCTION para designar funciones. Modula-2 es un caso algo especial, y utiliza la palabra PROCEDURE para designar cualquiera de las dos clases de subprogramas.

Las cabeceras de las funciones para los ejemplos anteriores podrían ser:

PROCEDURE Potencia( x: REAL; n: CARDINAL): REAL;
. . . . . .
PROCEDURE VolumenCubo( Lado: INTEGER ): INTEGER;
. . . . . .
PROCEDURE AreaTriangulo( Base, Altura: REAL ): REAL;
. . . . . .
PROCEDURE Distancia( x1, y1, x2, y2: REAL ): REAL;
. . . . . .

Estas cabeceras representan la interfaz entre la definición de la función y su utilización posterior. Los nombres de los argumentos son formales, esto quiere decir que no son variables del programa, sino sólo nombres simbólicos que sirven para formalizar la definición posterior de la función, permitiendo hacer referencia a los argumentos en la definición de los cálculos.

La definición completa de una función se compone de la cabecera, seguida de un cuerpo que tiene la misma estructura que un bloque de programa completo. Este bloque comienza con una parte declarativa y continúa con una parte ejecutiva introducida con la palabra BEGIN. En la parte declarativa se declaran las constantes y variables para el uso local de la función. La parte ejecutiva estará constituida por una secuencia de sentencias. La función finalizará con la palabra clave END y a continuación nuevamente el nombre de la función seguida de un punto y coma (;).

En las sentencias que constituyen la función se puede (y se debe) hacer uso de los argumentos formales declarados en su interfaz. Esto permite parametrizar los cálculos de la función para los valores particulares de los argumentos. Así, otra forma de ver las funciones es como "expresiones parametrizadas".

Por ejemplo, la definición completa de las funciones anteriores se realizaría de la siguiente forma:

PROCEDURE Potencia( x: REAL; n: CARDINAL ): REAL;
   VAR
      k: CARDINAL; p: REAL;
BEGIN
   p := 1.0;
   FOR k:=1 TO n DO
      p := p*x
   END;
   RETURN p
END Potencia;
. . . . .

PROCEDURE VolumenCubo( Lado : INTEGER ): INTEGER;
BEGIN
   RETURN Lado*Lado*Lado
END VolumenCubo;
. . . . .

PROCEDURE AreaTriangulo( Base, Altura: REAL ): REAL
BEGIN
   RETURN (Base * Altura) / 2.0;
END AreaTriangulo;
. . . . .

PROCEDURE Distancia( x1, y1, x2, y2 : REAL ): REAL;
   VAR
      incrX, incrY : REAL;
BEGIN
   incrX := x2 – x1; incrY := y2 – y1;
   RETURN sqrt(incrX*incrX + incrY*incrY);
END Distancia;

En estos ejemplos se observa la existencia de una nueva sentencia de Modula-2, iniciada con la palabra clave RETURN. Esta sentencia sirve para devolver como valor de la función el resultado de los cálculos realizados. Esta sentencia tiene la siguiente estructura:

                    RETURN Expresión

y provoca la finalización inmediata de la ejecución de la función. El resultado de la expresión debe ser un valor del tipo indicado en la declaración de la función. Dicho valor es el que se devuelve como resultado de la función.

La sentencia RETURN se puede insertar en cualquier punto de la parte ejecutable de la función. Además, es posible utilizar más de una sentencia RETURN en una misma función. La ejecución de la función acaba cuando se ejecuta cualquiera de las sentencias RETURN.

A continuación se muestra la definición de una función con varias sentencias de retorno.

PROCEDURE Maximo2( x, y: INTEGER ): INTEGER;
BEGIN
   IF x >= y THEN
      RETURN x
   ELSE
      RETURN y
   END
END Maximo2;

7.2.2 Uso de funciones
Para usar una función en los cálculos de un programa se invoca dicha función escribiendo su nombre y a continuación, entre paréntesis, los valores concretos de los argumentos, separados por comas. Esta invocación de la función representa un valor del tipo de la función, que podrá ser usado como operando en una expresión aritmética, o en general en cualquier parte del programa en que sea válido escribir una expresión de ese tipo.

Al invocar una función es obligatorio que los valores suministrados para los argumentos correspondan en número y tipo con los argumentos en la definición. La correspondencia de tipo significa que el tipo del argumento en la invocación sea compatibl en asignación con el tipo de argumento formal. Por ejemplo:

VolumenCubo(ladoCubo) > 27
. . . . .
valorPotencia := Potencia(base, exponente);
. . . . .
area := AreaTriangulo( Distancia(xA, yA, xB, yB), medidaAltura))
. . . . .

El resultado del volumen del cubo es un valor entero y se debe comparar con un valor entero (27). La variable valorPotencia tendrá que ser de tipo real, el argumento base debe ser de tipo real y el argumento exponente debe ser de tipo cardinal o entero.

El resultado del cálculo de la distancia entre los puntos A y B es de tipo real. En el cálculo del area del triángulo, el argumento para la base debe ser de tipo real. Por tanto, el resultado de la distancia entre A y B se puede utilizar como base del triángulo. La variable medidaAltura también debe ser de tipo real.

El efecto de la invocación de una función puede describirse en forma simplificada de la siguiente manera:

1) Se evalúan las expresiones de los valores de los argumentos.
2) Se asignan dichos valores a los correspondientes argumentos formales.
3) Se ejecuta el código de la definición de la función, hasta alcanzar una sentencia de retorno.
4) El valor retornado se usa en el punto donde se invocó la función.

Otros posibles efectos de la invocación de una función o procedimiento se describen más adelante.

7.2.3 Funciones predefinidas.
Modula-2 dispone de un conjunto de funciones predefinidas para algunas operaciones habituales. La lista completa de estas funciones es:

ABS( X )                CAP( C )              CHR( X )                 FLOAT( X )
HIGH( A )              MAX( T )               MIN( T )                  ODD( X )
ORD( X )               SIZE( T )               TRUNC( R )             VAL( T, X )

Algunas de estas funciones no pueden ser descritas por ahora, ya que utilizan elementos de Modula-2 que aún no se han explicado. Otras han sido ya utilizadas en temas anteriores. A continuación se da la descripción delas que se pueden utilizar con los elementos introducidos hasta el momento. En los argumentos simbólicos, X representa un valor numérico, C un carácter, y T un tipo.

ABS( X )              Valor absoluto de un número
CAP( C )             Carácter convertido a mayúscula
CHR( X )              Carácter de la tabla de caracteres en la posición X
FLOAT( X )          X convertido a valor REAL
MAX( T )              Valor máximo del tipo T
MIN( T )               Valor mínimo del tipo T
ODD( X )             Devuelve cierto cuando el valor de X es impar
ORD( X )             Posición que ocupa X en la lista de valores de su tipo
TRUNC( R )         Valor de R (REAL) truncado a entero
VAL( T, X )          X convertido al tipo T

Las funciones predefinidas en Modula-2 son, en general, seudofunciones. Esto es particularmente cierto para las funciones que usan tipos como argumentos o en que el tipo del argumento no está totalmente determinado (por ejemplo, admiten cualquier tipo numérico). Las funciones predefinidas son elementos básicos del lenguaje, al igual que los operadores aritméticos o de comparación (+ – * DIV MOD < =…). Las funciones predefinidas están, por tanto, disponibles sin necesidad de declararlas previamente. 

7.2.4 Funciones estándar
Además de las funciones predefinidas, al realizar programas en Modula-2 podremos utilizar las funciones que estén definidas en módulos ya redactados de antemano. algunos módulos se consideran estándar y deben estar disponibles junto con cada compilador de Modula-2, tal como ya se dijo al hablar de las operaciones de lectura y escritura.

Las funciones definidas en módulos estándar se denominan funciones estándar y pueden ser utilizadas sin necesidad de escribir su definición, pero (a diferencia de las funciones predefinidas) hay que indicar expresamente que se van a utilizar dichas funciones mediante una declaración IMPORT del módulo que las contenga.

En lo referente a funciones matemáticas, se dispone de un módulo estándar llamado MathLib0. Para utilizar sus funciones en un programa es necesario, como se ha dicho, indicar en la cabecera del programa cuáles de ellas se van a emplear, escribiendo una sentencia IMPORT para este módulo. Las funciones matemáticas disponibles en este módulo son las siguientes:

exp ( x )               Exponencial, e^x(e elevado a x)
ln( x )                   Logaritmo neperiano de x
sin( x )                 Seno de x
cos( x )                Coseno de x
arctan( x )             Arcotangente de x
sqrt( x )                Raíz cuadrada de x
entier( x )              Mayor entero <= x

Todas estas funciones tienen un argumento REAL y devuelven un valor REAL, excepto entier, que devuelve un valor del tipo INTEGER. La función sqrt ya ha sido utilizada en este mismo tema para la definición de la función Distancia. Esto se puede hacer después de importar en el módulo principal esta función desde el módulo MathLib0.

7.3 Procedimientos

Un procedimiento es un sobprograma que realiza una determinada acción. A diferencia de las funciones, un procedimiento no tiene como objetivo, en general, devolver un valor obtenido por cálculo.

Un procedimiento es una forma de subprograma que agrupa una sentencia o grupo de sentencias que realizan una acción, y permite darles un nombre por el que se las pueden identificar posteriormente. Estas sentencias, al igual que en las funciones, se pueden parametrizar mediante una serie de argumentos. Así, otra forma de ver a los procedimientos es como "acciones parametrizadas".

Por ejemplo, durante el desarrollo de un prograa podemos identificar acciones tales como:

          Trazar una línea de longitud dada
          Imprimir el resultado
          Ordenar dos valores
          Leer las coordenadas de un punto

que, si nos interesa, podremos desarrollar como procedimientos, y luego invocarlas en el programa cuando se necesite.

7.3.1 Definición de procedimientos
La definición en Modula-2 de un procedimiento es prácticamente igual a la de una función:

PROCEDURE Nombre( argumento: Tipo; … );
   declaraciones
BEGIN
   sentencias
END Nombre;

La diferencia principal es que no se declara el tipo de valor del resultado, ya que no existe dicho valor. Además, con cierta frecuencia interesa definir procedimientos sin argumentos. En estos casos sólo es necesario dar el nombre, y no habrá lista de argumentos ni paréntesis.

Como ejemplo, podemos dar posibles definiciones de procedimientos que correspondan a las dos primeras acciones citadas anteriormente.

PROCEDURE TrazarLinea ( longitud: INTEGER );
   VAR k: INTEGER;
BEGIN
   FOR k := 1 TO longitud DO
      Write( ‘-‘ )
   END
END TrazarLinea;

PROCEDURE EscribirResultado;
BEGIN
   WriteString( ‘Resultado’ );
   WriteReal( resultado, 10 )
END EscribirResultado;

En el primer caso se trata de un procedimiento para trazar una línea horizontal de cualquier longitud, a base de imprimir guiones. El resultado de este procedimiento no es un valor determinado, sino la acción de trazado de la línea. En el segundo caso el procedimiento se utiliza solamente para agrupar las sentencias que imprimen un resultado ya calculado.

En la definición de un procedimiento pueden usarse también sentencias de retorno, pero con un significado algo diferente que en el caso de las funciones. La sentencia

RETURN

se escribe ahora simplemente así, sin ninguna expresión que la acompañe, ya que no hay ningún valor que devolver. Esta sentencia sirve simplemente para terminar la ejecución del procedimiento en ese momento, y volver al punto siguiente a donde se invocó. Por ejemplo, lotra posible definición del procedimiento de imprimir un resultado sería:

PROCEDURE EscribirResultado;
BEGIN
   IF resultado < 0 THEN
      WriteString( ‘Problema no resuelto’ );
      RETURN
   END;
   WriteString( ‘Resutado:’ );
   WriteReal( resultado, 10 )
END EscribirResultado;

En este caso si la condición de la sentencia IF resulta cierta las sentencias finales de escritura no se ejecutarán, ya que la sentencia de retorno termina la acción del procedimiento en forma inmediata.

7.3.2 Uso de procedimientos
Para usar un procedimiento hay que invocarlo. Dicha invocación o llamada constituye por sí sola una sentencia de Modula-2, cuyo formato es:

Nombre( argumento, argumento, … )

Como puede observarse, un procedimiento se invoca escribiendo su nombre y a continuación, si los hay, los valores de los argumentos particulares en esa llamada, separados por comas. Los valores de los argumentos pueden darse, en general, mediante expresiones. Si no hay argumentosse suprimen también los paréntesis, con lo que la llamada a un procedimiento sin argumentos se reduce a su nombre.

Los argumentos en la llamada deberán ser compatibles con los indicados en la declaración, tal como se dijo para las funciones. Por ejemplo, los procedimientos declarados anteriormente podrían invocarse en la forma:

TrazarLinea( 3*Lado );
. . . . .
EscribirResultado;

con la primera llamada se trazará una línea con el triple de la longitud del Lado. Con la segunda llamada se escribirá el resultado según el formato programado en la definición de este procedimiento.

En forma simplificada, la invocación de un procedimiento produce un efecto análogo a la secuencia de acciones siguientes:

1) Se evalúan las expresiones de los valores de los argumentos.
2) Se asignan dichos valores a los correspondientes argumentos formales.
3) Se ejecuta el código de la definición del procedimiento, hasta alcanzar el final del bloque o una sentencia de retorno.
4) El programa que invocó al procedimiento continúa en el punto siguiente a la sentencia de llamada.

7.3.3 Procedimientos predefinidos
En Modula-2 también existen un conjunto de procedimientos predefinidos. Los nombres, argumentos y operaciones que realizan son las siguientes:

DEC( X )             Decrementa en 1 el valor de la variable X
DEC( X, N )         Decrementa en N el valor de la variable X
EXCL( S, X )        Excluye al elemento X del conjunto S
HALT                  Finaliza la ejecución del programa
INC( X )               Incrementa en 1 el valor de la variable X
INC( X, N )          Incrementa en N el valor de la variable X
INCL( S, X )         Incluye el elemento X en el conjunto S

Los procedimientos EXCL y INCL se comentarán en el tema dedicado al manejo de los conjuntos. El resto tienen la utilidad que se indica. Aquí es también aplicable el comentario hecho acerca de las funciones predefinidas, en cuanto a que los procedimientos predefinidos son en realidad seudoprocedimientos que forman parte del lenguaje en sí.

7.3.4 Procedimientos estándar
Al igual que para las funciones, en los módulos estándar asociados a cada compilador de Modula-2 se disponen de diversos procedimientos estándar que pueden utilizarse sin más que realizar la correspondiente importación.

En particular ya se han mencionado y utilizado procedimientos estándar de lectura de datos o escritura de resultados, que pueden importarse de los módulos InOut y RealInOut.

7.4 Paso de argumentos

La manera fundamental de comunicar información entre las sentencias de un subprograma y el programa que lo utiliza es mediante los argumentos. En Modula-2 existen dos formas distintas de realizar esta comunicación, que se denominan paso por valor y paso por referencia, y que se describen a continuación.

7.4.1 Paso de argumentos por valor
Esta es la forma utilizada en los ejemplos que se han mencionado hasta el momento. Los argumentos representan valores que se transmiten desde el programa que llama hacia el subprograma. En el caso de las funciones hay además un valor de retorno, que es el valor de la función que se transmite desde el subprograma hacia el programa que lo llamó.

Los argumentos reales en la llamada al subprograma pueden darse en general en forma de expresiones, cuyos tipos de valor deben ser compatibles en asignación con los tipos de los argumentos formales.

El modo de paso por valor implica que los elementos usados como argumentos en la llamada al subprograma no pueden ser modificados por la ejecución de las sentencias del subprograma. Esto es cierto incluso en el caso de que en el subprograma se ejecuten asignaciones a los argumentos formales, considerados como variables dentro del subprograma.

El paso de argumentos por valor puede describirse de la siguiente manera:

1) Se evalúan las expresiones de los argumentos reales usados en la llamada.
2) Los valores obtenidos se copian en los argumentos formales.
3) Los argumentos formales se usan como variables dentro del subprograma. Si a estas varibles se les asignan nuevos valores, no se estará modificando el argumento real, sino sólo la copia.

Por ejemplo, si se modifica la definición de la función para el cálculo de la distancia entre dos puntos de la siguiente forma:

PROCEDURE Distancia( x1, y1, x2, y2 : REAL): REAL;
BEGIN
   x1 := x2- x1; y1 := y2 – y1;
   RETURN sqrt(x1*x1 + y1*y1);
END Distancia;

y se tiene un fragmento de programa tal como:

. . . . .
xA := 23.5; yA := 12.3;
xB := 5.7; yB := 2.6;
distanciaAB := Distancia(xA, yA, xB, yB);

después de la ejecución total del fragmento, las variables xA e yA mantienen sus valores antes de la llamada.
Ya que la reasignación de valor a un argumento pasado por valor resulta algo confusa, es preferible evitar esta circunstancia todo lo posible.

7.4.2 Paso de argumentos por referencia
En ciertos casos es deseable que el subprograma pueda modificar las variables que se usen como argumentos. Esto permite producir simultáneamente varios resultados y no sólo uno. El mecanismo de paso por valor no permite que el subprograma modifique directamente una variable pasada como argumento. Para conseguirlo hay que usar el otro modo de paso de argumentos, denominado paso por referencia.

El paso de un argumento por referencia se indica en la cabecera del subprograma, anteponiendo la palabra clave VAR al nombre del argumento formal, en la forma:

PROCEDURE Nombre( VAR agumento: Tipo … ) …

Si un argumento se pasa por referencia, ya no será válido usar como argumento real una expresión. El argumento real usado en la llamada debe ser necesariamente una variable del mismo tipo. Esta variableserá utilizada en el subprograma como si fuera suya.

El paso de argumentos por referencia puede describirse de la siguiente manera:

1) Se seleccionan las variables usadas como argumentos reales.
2) Se asocia cada variable con el argumento formal correspondiente.
3) Se ejecutan las sentencias del subprograma como si los argumentos formales fuesen los argumentos reales.

Ahora se pueden escribir las definiciones como subprograma de las restantes acciones puestas como ejemplo al hablar de procedimientos en el apartado 7.3.

        Ordenar dos valores
        Leer las coordenadas de un punto

ya que en ellas necesitamos utilizar argumentos pasados por referencia en los que se puedan dejar los valores ordenados o las coordenadas leídas.

PROCEDURE OrdenarDos( VAR y, z : INTEGER );
   VAR aux : INTEGER;
BEGIN
   IF y > z THEN aux := y; y := z; z := aux END
END OrdenarDos;
. . . . .

PROCEDURE LeerCoordenadas( Punto :CHAR; VAR X, Y : REAL);
BEGIN
   WriteString( "Punto" ); Write(Punto); WriteLn;
   WriteString( "¿Coordenada X ?" );
   ReadReal( X ); WriteLn;
   WriteString( "¿Coordenada Y ?" );
   ReadReal( Y ); WriteLn;
END LeerCoordenadas;

Su utilización sería la siguiente

. . . .
OrdenarDos(A,B);
. . . .
LeerCoordenadas("A", xA, yA);
. . . .

Entre los procedimientos predefinidos y los procedimientos estándar podemos encontrar casos en que los argumentos se pasan por referencia. Esto ocurre con los procdimientos predefinidos DEC, INC y los procedimientos de lectura de los módulos InOut y RealInOut.

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

 

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 primera unidad didáctica. Todos ellos deben ser realizados en Modula-2 utilizando la metodología explicada a lo largo de esta unidad didáctica. El grado de dificultad de los ejercicios es creciente y conviene realizarlos siguiendo el orden de los enunciados. Es muy importante que la realización de los ejercicios se lleve a cabo en dos etapas:

1ª.- Se debe plantear sobre el papel la solución del ejercicio, empleando la técnica de refinamientos sucesivos explicada en esta unidad.

2ª.- Comprobar en el computador la solución adoptada.

Inicialmente, para conseguir cierta soltura en el manejo del computador y el compilador de Modula-2, es aconsejable comprobar algunos de los ejercicios ya resueltos a lo largo de la unidad. Los enunciados de los ejercicios son los siguientes:

1.- Realizar un programa que imprima la tabla de multiplicar por un número leído como dato. Por ejemplo, si se quiere obtener la tabla de multiplicar del 9 se tendría la siguiente hoja de resultados:

¿Número? 9

Tabla de multiplicar por 9
==================
        9  x  1  =   9
        9  x  2  =  18
        9  x  3  =  27
        9  x  4  =  36
        9  x  5  =  45
        9  x  6  =  54
        9  x  7  =  63
        9  x  8  =  72
        9  x  9  =  81
        9  x 10 =  90


2.- Realizar un programa para calcular el máximo común divisor de dos números enteros. Por ejemplo:

¿Primer Número? 655
¿Segundo Número? 1325

El máximo común divisor es:    5


3.- Realizar un programa que escriba un rombo simétrico de asteriscos como el que se muestra a continuación, tomando como dato el número de asteriscos que tiene el lado.

¿Lado? 4

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


4.- Realizar un programa que calcule el número e mediante el desarrollo en serie:

Ejercicio4_unidad1

con un error menor del introducido como dato. Por ejemplo:

¿Error ? 0.000001

Valor de e = 2.71828198


5.- Realizar un programa que lea la longitud de los tres lados de un triángulo y analice qué tipo de triángulo es. Los resultados posibles serán los siguientes:

  • No forman triángulo (un lado mayor que la suma de los otros dos)
  • Triángulo equilátero (tres lados iguales)
  • Triángulo isósceles (dos lados iguales)
  • Triángulo escaleno (tres lados distintos)
  • Triángulo rectángulo (sus lados cumplen el teorema de Pitágoras)


6.- Realizar un programa que analice un texto terminado con el carácter punto ( . ) y contabilice los siguientes aspectos:

  • Número total de caracteres
  • Número total de vocales utilizadas
  • Total de veces utilizada la vocal "a" mayúscula o minúscula
  • Total de veces utilizada la vocal "e" mayúscula o minúscula
  • Total de veces utilizada la vocal "i" mayúscula o minúscula
  • Total de veces utilizada la vocal "o" mayúscula o minúscula
  • Total de veces utilizada la vocal "u" mayúscula o minúscula


7.- Realizar un programa que dado un número N, introducido como dato, escriba todos los números comprendidos entre 1 y 10000 que cumplan las dos reglas siguientes:

1ª regla: La suma de sus cifras debe ser un divisor de N
2ª regla: El producto de sus cifras debe ser un múltiplo de N


8.- Realizar un programa que a partir del capital ( C ), el tanto por ciento de interés anual ( I ) y los años de amortización ( A ) de un crédito, introducidos como datos, calcule la anualidad fija a pagar a lo largo de los A años. La fórmula para este cálculo es la siguiente:

Ejercicio8_unidad1

El programa también debe calcular para todos los años la parte de la anualidad dedicada al pago de interés y la parte dedicada a la amortización de la deuda. Por ejemplo:

¿Capital? 1000000
¿Interés (%) ? 15
¿Años? 3

Anualidad: 437977

Año             Intereses             Amortización
 1                 150000                287977
 2                 106804                331173
 3                   57127                380850

EL TORNEO DE AJEDREZ

En un torneo de ajedrez participaron 30 concursantes que fueron dividos de acuerdo a su categoría en dos grupos. En cada uno de los grupos los participantes jugaron una partida contra todos los demás. En total se jugaron 87 partidas más en el segundo grupo que en el primero. El ganador del primer grupo no perdió ninguna partida y totalizó 7,5 puntos.

¿En cuántas partidas hizo tablas el ganador?

::SOLUCIÓN::
aquí

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

 

6.2.2.2 Escritura mediante bucle de líneas
La segunda alternativa de diseño consiste en plantear la escritura del triángulo como un bucle de imprimir líneas de números, una tras otra. El refinamiento correspondiente será:

            Imprimir el triángulo hasta N —>
                 WHILE quedan números DO
                    Imprimir la siguiente línea
                 END

Para ir imprimiendo cada línea necesitaremos conocer el rango de números que le corresponde. Usaremos unas variables similares a las de la alternativa anterior, para mantener el número de la línea, y el primer y último número de esa línea:

            VAR   linea, primero, ultimo: INTEGER;

Al comienzo del bucle estas variables tendrán los valores correspondientes a la última línea impresa. Los valores iniciales serán:

            linea := 0;
            primero := 0;
            ultimo := 0;

Ahora podemos refinar los elementos del bucle:

            quedan números —> ultimo < N

            imprimir la siguiente línea —>
                   actualizar los límites
                   imprimir los números

            actualizar los límites —>
                   linea := linea + 1;
                   primero := ultimo + 1;
                   ultimo := ultimo + linea;

            imprimir los números —>
                   FOR k := primero TO ultimo DO
                      WriteInt( k, 5 )
                   END;
                   WriteLn

Con esto se completa el programa; pero antes de presentarlo en su conjunto es preciso realizar alguna corrección. Dicha corrección es debida a que al calcular los límites de una línea de números no se ha tenido en cuenta que la última línea puede no estar completa. El último número de la línea no debe ser nunca mayor que el último de la serie completa. Escribiremos, por tanto:

             actualizar los límites —>
                  INC( linea );
                  primero := ultimo+1;
                  ultimo := ultimo + linea;
                  IF ultimo > N THEN
                      ultimo := N
                  END;

Ahora sí se puede construirse el programa Floyd2, y presentarlo debidamente documentado tal como se hace en el siguiente listado. El resultado de la ejecución de este programa es idéntico al de la variante anterior. Ambos programas son equivalentes desde el punto de vista de su utilización.

(*************************************************************************
*
*    Programa: Floyd2
*
*    Descripción:
*      Este programa imprime el triángulo de Floyd con los números
*      correlativos de 1 a N. El valor de N se lee como dato.
*
*************************************************************************)
MODULE Floyd2;

(*========================================================================
    IMPORTACIÓN Y DECLARACIONES DEL PROGRAMA
========================================================================*)
  FROM InOut IMPORT WriteLn, WriteString, ReadInt, WriteInt;
  VAR
    N: INTEGER;                (* último número a imprimir *)
    k: INTEGER;                (* cotandor de números *)
    linea: INTEGER;            (* contador de línea *)
    primero: INTEGER;            (* primer número de la línea *)
    ultimo: INTEGER;            (* último número de la línea *)

BEGIN

(*=======================================================================
    PARTE EJECUTABLE DEL PROGRAMA
=======================================================================*)
  (*– leer el límite de la serie –*)
    WriteString( "Límite de la serie: " );
    ReadInt( N );
    WriteLn;

  (*– imprimir el triángulo mediante un bucle de líneas –*)
    k := 0;
    linea := 0;
    primero := 0;
    ultimo := 0;
    WHILE ultimo < N DO
      (*– actualizar los límites –*)
    linea := linea+1;
    primero := ultimo+1;
    ultimo := ultimo + linea;
    IF ultimo > N THEN
      ultimo := N
    END;
      (*– imprimir los número –*)
    FOR k := primero TO ultimo DO
      WriteInt( k,5 )
    END;
    WriteLn
    END
END Floyd2.


6.3 Verificación de programas

Ya se ha indicado que uno de los objetivos de la programación es la corrección. Un programa es correcto si produce siempre resultados de acuerdo con la especificación del programa. Evidentemente, sólo tiene sentido hablar de corrección si antes de escribir el programa se ha escrito de manera precisa la especificación del comportamiento que se espera que tenga.

En la práctica, la verificación de un programa se hace muchas veces mediante ensayos. Un ensayo consiste en ejecutar el programa con unos datos preparados de antemano y para los cuales se sabe cuál ha de ser el resultado a obtener. Si al ejecutar el programa no se obtienen los resultados esperados, se sabrá que hay algún error, y el programa se examina para determinar la causa del error y eliminarla. Este proceso se llama depuración (en inglés "debugging").

Si la ejecución produce los resultados esperados, entonces el ensayo no suministra ninguna información acerca de la corrección del programa. Puede ser que el programa sea correcto, y no tenga errores, pero también puede ocurrir que el programa contenga errores que sólo se pongan de manifiesto con otros datos de ensayo diferentes.

La única manera de verificar con seguridad la corrección de un programa es demostrar formalmente que el programa cumple con sus especificaciones. Para ello es necesario escribir esas especificaciones con toda precisión en forma de expresiones lógicas, y luego realizar una demostración lógico-matemática de que el programa las cumple. A continuación se exponen algunas ideas sobre cómo puede ser este proceso de demostración.

6.3.1 Corrección parcial y total
Usando las expresiones y reglas de la lógica, y conociendo la semántica (significado) de las acciones, es posible demostrar si un programa es o no correcto. Para programas que siguen el modelo imperativo el proceso de demostración se realiza en dos partes:

1) CORRECCIÓN PARCIAL: si el programa termina el resultado es correcto.

2) CORRECCIÓN TOTAL: para todo dato de entrada válido el programa termina.

La base de la comprobación de corrección parcial es:

  • Anotar el comienzo y final del programa con aserciones (afirmaciones, formalizadas como expresiones lógicas) correspondientes a las condiciones iniciales y al resultado deseado.
  • Anotar los puntos intermedios del programa con asersiones similares respecto al estado del cómputo en ese punto.
  • Demostrar que si se cumple una aserción en un punto del programa y se siguen cada una de las líneas de ejecución posible hasta llegar a otro punto con aserción, dicha aserción ha de cumplirse, según las reglas de la lógica y de acuerdo con las acciones realizadas.

En particular, la corrección parcial se consigue al demostrar que al llegar al final del programa ha de cumplirse la aserción correspondiente al resultado deseado.

La corrección total se consigue añadiendo la demostración de que todos los bucles del programa terminan tras un número finito de repeticiones. Para demostrar la terminación se puede:

  • Asociar a cada bucle una función monótona (siempre creciente o decreciente) y que debe tener un valor acotado para que el bucle se repita.

De esta manera tras un cierto número de repeticiones se alcanzará la cota o límite de dicha función, y el bucle terminará.

6.3.2 Razonamiento sobre sentencias de asignación
Para analizar el comportamiento de un fragmento de programa correspondiente a una sentencia de asignación, comenzaremos por anotar delante de dicha sentencia todas las condiciones que sabemos que se cumplen inmediatamente antes de ejecutarla. A continuación anotaremos detrás de la sentencia las condiciones que podamos demostrar que se cumplen después de su ejecución, y que será las siguientes:

  • Las condiciones anteriores en las que no intervenga la variable asignada.
  • La condición de que la variable tiene el valor asignado.

Las condiciones se suelen anotar entre llaves { }. Por ejemplo:

{ (x > y) AND (a > b) AND (a > x) }
a := 36
{ (x > y) AND (a = 36) }

En la anotación final hemos suprimido las condiciones iniciales (a > b) y (a > x), ya que estas variables no son afectadas por la asignación, y hemos añadido la nueva condición (a = 36) impuesta por la ejecución sentencia de asignación.

6.3.3 Razonamiento sobre el esquema de selección
Para analizar el comportamiento de un fragmento de programa correspondiente a un esquema de selección, comenzaremos por anotar delante de dicho esquema las condiciones que sepamos que se cumplen inmediatamente antes de examinar la condición.

 Figura(6_1)
      Figura 6.1 Razonamiento sobre un esquema de selección.

Puesto que la condición de selección decide la continuación por una u otra vía de las dos posibles, deduciremos que al comienzo de la alternativa "SI" se cumplirán las condiciones iniciales y además la condición de selección, y que al comienzo de la alternativa "NO" se cumplirán las condiciones iniciales y no se cumplirá la condición selección. La Figura 6.1 (a) refleja gráficamente estas anotaciones.

En la parte de terminación del esquema anotaremos las condiciones que se deduzcan de la ejecución de cada alternativa en particular; y anotaremos como condición a la salida que ha de cumplirse alguna de las condiciones de terminación, correspondientes a las dos alternativas posibles, tal como se indica en la Figura 6.1 (b).

Aplicaremos esta técnica de razonamiento a  un fragmento de programa que calcule en m el máximo de dos números a y b. Dicho fragmento podría ser:

IF a > b THEN
   m := a
ELSE
   m := b
END

Anotando las condiciones al comienzo, tendremos:

IF a > b THEN
   { a > b }
   m := a
ELSE
   { a <= b }
   m := b
END

Razonando sobre cada sentencia de asignación, de acuerdo con las propiedades matemáticas del máximo de dos valores, tendremos:

IF a > b THEN
   { a > b }
   m := a
   { (a > b) AND (maximo = a) }   ===> m = Max(a,b)
ELSE
   { a <= b }
   m := b
   { (a <= b) AND (maximo = b) }   ===> m = Max(a,b)
END

Simplificando, podremos anotar las condiciones de salida que nos interesan:

IF a > b THEN
   m := a
   { m = Max(a,b) }
ELSE
   m := b
   { m = Max(a,b) }
END
{ m = Max(a,b) }

6.3.4 Razonamiento sobre bucles: invariante, terminación
Para analizar el comportamiento de un fragmento de programa correspondiente a un esquema de iteración habremos de identificar las condiciones que deben cumplirse siempre inmediatamente antes de examinar la condición de repetición. Estas condiciones constituyen el llamado invariante del bucle. En la Figura 6.2 se representa el diagrama de flujo de un bucle tipo WHILE, en que el invariante es la condición { p }.

Figura(6_2)
     Figura 6.2 Razonamiento sobre un esquema de iteración.

Razonando como en el esquema de selección, deduciremos que al comienzo de cada repetición de la acción del bucle habrá de cumplirse el invariante y además la condición de repetición; y que al terminar las repeticiones y salir del bucle se cumplirá el invariante y además no se cumplirá la condición de repetición.

La identificación del invariante puede ser complicada. No se trata simplemente de anotar todas las condiciones que sabemos que se cumplen al llegar al bucle por primera vez, sino precisamente aquellas que además se seguirán cumpliendo después de cada repetición. La denominación de invariante responde al hecho de que estas condiciones no cambian al ejecutar la acción del bucle.

Como ejemplo, analizaremos un fragmento de programa para calcular en f el factorial de un número n (n! = 1x2x3..xn). Para ello se usará un contador k que vaya tomando los valores de 1 a n:

k := 1;
f := 1;
WHILE k<n DO
   k := k + 1;
   f := f * k
END

Usaremos como invariante { (k <= n) AND (f = k!) }. Esto es válido para los casos en que (n>=1). Las anotaciones en el bucle serán:

k := 1;
f := 1;
{ (k <= n) AND (f = k!) }
WHILE k<n DO
{ (k < n) AND (f = k!) }
   k := k + 1;
{ (k <= n) AND (f = (k-1)!) }
   f := f * k
{ (k <= n) AND (f = k!) }
END
{ (k = n) AND (f = k!) }     ===> f = n!

La demostración de las anotaciones es sencilla, conociendo propiedades matemáticas tales como:

1! = 1
(k < n)  —> (k+1 <= n)
k! = (k-1)! x k
(k <= n) AND (k >= n) —> k=n

El análisis anterior sólo es válido si (n >= 1). Falta comprobar la corrección para el caso (n = 0), ya que para (n < 0) el factorial no está definido. Sabiendo que 0! = 1, y comprobando que si (n = 0) el bucle no se ejecuta nunca, y la variable f conserva su valor inicial f = 1 = 0! tenemos la demostración completa.

Los razonamientos anteriores corresponden sólo a la demostración de corrección parcial. Para demostrar la corrección total hay que demostrar que el bucle termina siempre tras un número finito de repeticiones. Hay que buscar una función monótona acotada que varíe con cada repetición del bucle.

En este caso la función es sencillamente k. A cada repetición k aumenta una unidad. La cota superior para la que ya no se repite el bucle es n. Tras un cierto número de repeticiones k alcanzará forzosamente dicha cota y las iteraciones finalizarán.


6.4 Eficiencia de programas. Complejidad algorítmica.

Ya se ha dicho que el objetivo prioritario de la programación es la corrección. La eficiencia sólo debe tenerse en cuenta si es un factor decisivo o importante en cada caso. Aunque en la primera redacción de un programa no conviene prestar excesiva atención a los aspectos de eficiencia, tampoco debe descuidarse totalmente este aspecto. Por esta razón se presentan a continuación los elementos básicos de la eficiencia de programas, y una descripción informal de algunas técnicas para analizar dicha eficiencia.

6.4.1 Medidas de eficiencia
La eficiencia de un programa se analiza un función de la cantidad de recursos que consume durante su ejecución. Un programa eficiente consume pocos recursos, mientras que un programa menos eficiente consumirá una mayor cantidad de recursos. Esto quiere decir que establecer una medida de la eficiencia de los programas equivale a establecer una medida de los recursos usados durante su ejecución.

Las principales medidas de recursos empleados son:

  • El tiempo que tarda en ejecutarse un programa.
  • La cantidad de memoria usada para almacenar datos.

En muchos casos estos dos factores son mutuamente dependientes. Es decir, se pueden desarrollar programas que obtengan los resultados en menos tiempo, a costa de usar una mayor cantidad de memoria para almacenar datos, y viceversa.

En lo que sigue atenderemos exclusivamente a la primera de las dos medidas mencionadas. Un programa se considerará tanto más eficiente cuanto menos tiempo tarde en ejecutarse. Los programas poco eficientes tardarán mucho tiempo en dar los resultados. Hablaremos, por tanto, de la eficiencia en tiempo de un programa.

El tiempo de ejecución de un programa depende, en la mayoría de los casos, de los datos particulares con los que opera. Esto quiere decir que la eficiencia de un programa debe establecerse no como una magnitud fija para cada programa, sino como una función que nos dé el tiempo de ejecución para cada tamaño o cantidad de los datos que deba procesar.

Esta idea nos lleva, por su parte, a la necesidad de establecer previamente una medida del tamaño de los datos o tamaño del problema, para, en función de ella, establecer la medida de la eficiencia del programa que los procesa.

El tamaño del problema se puede expresar bien por la cantidad de datos a tratar, o bien por los valores particulares de los datos. Por ejemplo, para un programa que obtenga el valor medio o la suma de una serie de datos, el número de datos a sumar o promediar es una buena medida del tamaño del problema. En cambio, para un programa que calcule una potencia de un número, el tamaño significativo puede ser el valor del exponente.

La función que da el tiempo de ejecución según el tamaño del problema se dice que mide la complejidad algorítmica del programa.

6.4.2 Análisis de programas
La determinación de la eficiencia (o complejidad) de un programa se hace analizándolos siguientes elementos:

1) Cuánto tarda en ejecutarse cada instrucción básica del lenguaje utilizado.

2) Cuántas instrucciones de cada clase se realizan durante una ejecución del programa.

Para simplificar, consideraremos que cada operación elemental del lenguaje de programación: suma, resta, lectura, escritura, asignación de valor, decisión  según condición, etc…, dura una unidad de tiempo. Con esta simplificación, el análisis de la eficiencia de un programa se centra en establecer cuántas instrucciones se ejecutan en total, dependiendo del tamaño o cantidad de los datos a procesar.

Al realizar el análisis mencionado nos encontraremos con que el número preciso de instrucciones ejecutadas depende de los valores particulares de los datos, incluso para un tamaño fijo del problema. En este caso se pueden adoptar al menos dos criterios diferentes para realizar el análisis de eficiencia:

  • Analizar el comportamiento, en promedio.
  • Analizar el comportamiento en el peor caso.

Utilizaremos el segundo criterio, por ser el más sencillo de aplicar, para analizar algunos ejemplos de programas. Con este criterio el análisis de complejidad (número de instrucciones ejecutadas) de los esquemas básicos de los programas es el siguiente:

1) La complejidad de un esquema secuencial es la suma de las complejidades de sus acciones componentes.

2) La complejidad de un esquema de selección equivale a la de la alternativa más compleja, es decir, de ejecución más larga, más la complejidad de la evaluación de a condición de selección.

3) La complejidad de un esquema de iteración se obtiene sumando la serie correspondiente al número de instrucciones en las repeticiones sucesivas.

Veamos cómo es este análisis en algunos casos concretos. Tomemos como ejemplo el siguiente fragmento de programa que obtiene el máximo de dos números:

maximo := a;
IF a < b THEN
   maximo := b
END

El esquema global es una secuencia de dos acciones: una asignación, seguida de un esquema condicional (IF). Anotaremos el programa con el número de instrucciones correspondientes a cada sentencia, para lo cual contaremos el número de operadores (+,-,*,<,:=, etc.) y decisiones (IF, ELSIF,WHILE, etc.). Aplicando las reglas para los esquemas tendremos:

maximo := a;                                1
IF a < b THEN         2   )
   maximo := b        1   )                  3 (Regla 2)
END
                                Total = 4            (Regla 1)

La complejidad en este caso es fija, y no depende de una medida de tamaño del problema.

A continuación analizaremos un bucle que obtiene en f el factorial de un número k. Anotaremos el programa con el número de instrucciones de cada sentencia:

k := 1;                                                              1
f := 1;                           \                                   1
WHILE k<n DO        2    |
   k := k + 1;            2    |                             6(n-1) (Regla 3)
   f := f * k                2    |
END                             /
                                        Total =        6n – 4 (Regla 1)

Para calcular el número de instrucciones del bucle se ha multiplicado el número de instrucciones en cada repetición por el número de repeticiones. La complejidad aparece expresada en función del valor de n, que en este caso resulta una medida natural del tamaño del problema.

6.4.3 Crecimiento asintótico
En los análisis de eficiencia (o complejidad) se considera muy importante la manera como la función de complejidad va aumentando con el tamaño del problema. Lo que interesa es la forma de crecimiento del tiempo de ejecución, y no tanto el tiempo particular empleado. Como ejemplo podemos comparar dos programas, uno que tarde un tiempo 100N en resolver un problema de tamaño N, y otro que tarde un tiempo N^2. La comparación puede hacerse escribiendo una tabla con los tiempos de cada uno para diferentes tamaños del problema.

      Tamaño        100N            N^2
         1               100              1
         2               200              4
         3               300              9
         10             1.000           100
         100           10.000          10.000
         1.000        100.000        1.000.000

Al principio de la tabla el primer programa parece menos eficiente que el segundo, ya que tarda mucho más tiempo, pero a medida que aumenta el tamaño del problema ambos programas llegan a tardar lo mismo (para tamaño 100), y a partir de ahí el segundo programa demuestra ser mucho menos eficiente que el primero.

La menor eficiencia del segundo programa para tamaños grandes del problema no cambia el hecho de que se modifique el coeficiente multiplicador. Si el primer programa tardase 10 veces más (1.000N en lugar de 100N), acabaría igualmente por resultar mejor que el segundo a partir de un cierto tamaño del problema.

Lo que importa es la forma de la función, que en el primer caso es lineal, y en el segundo es cuadrática. La forma en que crece la función para tamaños grandes se dice que es su comportamiento asintótico, y se representa mediante la notación:

            O( f(n) )

siendo n el tamaño del problema, f la forma o función de crecimiento asintótico, y O (que se lee O-mayúscula) indica el orden de crecimiento. Algunas formas de crecimiento típicas, y sus valoraciones habituales, son las siguientes:

O( 1 )                     Complejidad constante. Ideal.
O( log n )                Crecimiento logarítmico. Muy eficiente.
O( n )                     Complejidad lineal. Muy eficiente.
O( n log n)              Eficiente.
O( n^2 )                  Complejidad cuadrática. Menos eficiente.
O( n^k )                  Complejidad polinómica. Poco eficiente.
O( 2^n )                  Complejidad exponencial. Muy poco eficiente.

Las valoraciones indicadas son, por supuesto, muy imprecisas. Hay muchos problemas que sólo pueden resolverse mediante programas de complejidad elevada, poco eficientes según esta valoración, pero adecuados para esos problemas. Los problemas que sólo pueden resolverse con programas de complejidad exponencial se consideran intratables en la práctica para tamaños grandes del problema.

Chatterbots (artículo revista)

La ciencia ficción se hace realidad

La educación que recibimos durante nuestra vida, así como la experiencia que cada uno va acumulando, hacen que, por ejemplo, distingamos entre un círculo y un cuadrado, identifiquemos la cara de nuestro padre, sepamos que si tocamos una llama con el dedo nos quemaremos, o que si el cielo se nubla lo más probable será que llueva. Para cada una de estas situaciones y para muchas otras más, existen especializaciones dentro de la I.A. que intentan que las máquinas respondan de la misma manera que lo haría una persona, o que sigan su mismo proceso de razonamiento.

Antes de que empecemos a hablar de los bots, es importante que aclaremos algunos términos. Para empezar, ¿qué es la Inteligencia Artificial? (en el resto del artículo me referiré a ella por I.A.). A pesar de lo que hayáis podido ver en multitud de películas, el concepto en sí es mucho más sencillo. Genéricamente puede definirse como la adaptación de la forma en que piensa el ser humano, resuelve problemas, adquiere conocimiento y razona, a un programa o sistema informático. Lo realmente complicado es plasmar sobre un papel alguno de estos mecanismos.
   Los Chatterbots enlazan con el estudio del lenguaje natural, pero está claro que la necesidad de nuevos interfaces entre persona  máquina harán que este tipo de bots siga evolucionando. Como su propio nombre indica, los Chatterbots son bots que chatean contigo. Un bot es un programa, más o menos complejo, que se carga dentro de otro sistema y desarrolla de forma autónoma alguna labor. En muchas ocasiones emula el comportamiento de un usuario. Si sois jugones, recordaréis, por ejemplo, la colección de bots que salieron para el CounterStrike en cada una de sus versiones. Cargabas unos cuantos en cada uno de los dos equipos, y la diversión no tenía fin. Llegaban incluso a responder a los mensajes de auxilio, se enfadaban si disparabas a los de tu propio equipo, se escondían cuando eran los últimos, acechando al enemigo y, por supuesto, no ahorraban en insultos, chascarrillos y burlas. Los bots a los que ahora nos referimos, se limitan únicamente a reaccionar ante entradas de texto, a las que responden con salidas de texto, más o menos coherentes.

EN QUÉ CONSISTE
Uno de los tres puntos más importantes en un chatterbot consiste en que extraiga el máximo de información de la conversación. Cuanto mayor sea la cantidad de información que extrae de nuestras frases, la precisión y corrección de las respuestas será mayor. Lo que ha permitido un mayor desarrollo de este tipo de herramientas de texto que su equivalente hablado, es el medio que utilizan. El uso del medio escrito para mantener la conversación permite que, dejando de lado la ambigüedad propia del lenguaje, el programa pueda trabajar sobre palabras perfectamente acotadas, y que, independientemente de la persona que las teclee, siempre se escribirán igual (faltas de ortografía aparte). El desarrollo de un programa o sistema que permita mantener una conversación hablada se enfrenta con las limitaciones que el reconocimiento de la señal de voz conlleva, que son muchas.
   La segunda característica reside en que sea capaz de dar respuestas correctas tanto en la forma como en el fondo y acordes con el contexto en el que se habla. Es decir, si las frases que salen de la máquina, por correctas que sean, no tienen nada que ver con la conversación mantenida, estaríamos frente a algo parecido a un loro mecánico. Sí, la sintaxis, el léxico y la semántica del texto son correctos, pero sencillamente la frase no viene a cuento en laa conversación que mantenemos.
   En tercer lugar se encuentra el hecho de que responda en un tiempo razonable y nos permita mantener una conversación más o menos fluida. Como ejemplo tomaremos al típico programa de ajedrez. Si es bueno y tiene unos algoritmos de búsqueda decentes y le damos todo el tiempo del mundo para que calcule la mejor jugada, lo más probable es que tarde una media de 10 minutos por cada una. Cierto es que ganará la partida, pero el tiempo de respuesta en el caso que tratamos es demasiado importante, lo que nos obliga a un equilibrio entre calidad y tiempo de la respuesta.
   Suponiendo que hayamos conseguido un bot con las anteriores características, podremos someterlo al test de Turing, que consiste en el siguiente experimento. Ponemos a una persona en una habitación y a la máquina en otra. Hacemos que la persona, chatee con ella, pero sin que ésta sepa si conversa con una persona o una máquina. Pasado un tiempo determinado, se le pregunta a la persona con quién cree que chateaba. Si el encuestado es incapaz de distinguirlo, nuestro bot habrá pasado la prueba. Pero, a día de hoy no se conoce ningún bot que haya conseguido pasarla.
   Existen concursos internacionales con premios en metálico en los que se evalúa a los bots participantes, haciéndoles preguntas sobre temas de actualidad y sobre el mundo real. En función de la calidad de las respuestas, se les va puntuando, y finalmente se hace con la victoria el que más puntos acumule.
   Contamos con otra prueba para distinguir a humanos de máquinas, conocida como "CAPTCHA" (Completely Automated Public Turing Test to tell Computers And Humans Apart). Aunque se sale un poco del tema de los chatterbots, veamos en qué consiste. A pesar de lo aparatoso del nombre, en algún momento habréis visto algo similar. Consiste en una imagen, generalmente de texto bastante distorsionado, sobre un fondo no uniforme. Suele incluirse en páginas de creación de cuentas de correo, encuestas, etcétera. Con ello se evita, por ejemplo, que algún bot se dedique a crear cuentas de correo de forma masiva. Un ejemplo:

ejemplo1 
   Para un bot, reconocer el nombre escrito entre esta marea de líneas es casi imposible. Para una persona que sepa leer y no tenga demasiadas dioptrías, reconocer las primeras letras no es difícil, y a partir de éstas, puede imaginar el resto, con lo que reconocerlas, si ya tiene una idea previa, no debería ser complicado. Hoy en día tampoco existe ningún sistema capaz de resolver una colección aleatoria de capuchas.

CÓMO FUNCIONA
Los elementos que lo forman, como si de un sistema experto se tratara, consisten en una o varias bases de conocimiento que almacenan el grueso de los datos, y un motor que se encarga de tratarlos. Esta forma de construir sistemas inteligentes resulta muy cómoda, ya que permite modificar las bases de conocimiento o el motor de tratamiento de forma independiente. A partir de estos elementos, nos planteamos las siguientes cuestiones: ¿cómo extrae información el bot de lo que nosotros decimos?, ¿cómo elije la respuesta más apropiada en cada caso?, ¿realmente nos entiende?
   La información la extrae aplicando métodos de análisis del lenguaje natural. Normalmente, y por ser el más asequible, se emplea el método de las palabras clave, que consiste en analizar cada frase de entrada y, en función de ciertas palabras que aparecen en ella, orientar la conversación hacia un tema u otro. Por ejemplo, normalmente al iniciar una conversación, la colección de palabras que utilizamos es bastante limitada. Podemos empezar con el típico "hola", un "buenas", un "buenos días" y, como mucho, un "¿qué pasa?". Todas ellas serán palabras clave. En el caso del saludo inicial, las palabras tienen además una característica, y es que aparecerán al principio de la conversación. Si se encuentran más adelante, excepto el "hola", es bastante probable que su sentido no sea el de saludarnos. Pues bien, si el bot, al analizar la frase de entrada comprueba que ésta incluye un "hola", responderá alguna de las frases programadas para responder al saludo: "hola amigo", "me acabas de despertar…" o cualquiera otra que se os ocurra. Un detalle a tener en cuenta es que la programación del bot se orienta a que haga creer al usuario que realmente está manteniendo una conversación con una persona. Las limitaciones actuales impiden que realmente el bot extraiga toda la información que contienen las frases de entrada, luego, como es incapaz de entender exactamente lo que le decimos, se programa para que responda con frases que nos resulten satisfactorias. Por ejemplo, si, usando el método de las palabras clave, el robot detecta la plabra "deporte" en una frase, aunque no haya entendido nada de lo que le hemos preguntado, puede responder: "Mi deporte favorito esel fútbol". Dicho así suena un poco simple, pero es una salida bastante buena. Como podéis imaginar, el método de las palabras clave es muy efectivo cuando se utilizan frases cortas, pero pierde bastante eficacia en el momento en el que la longitud de la frase aumenta, ya que en una misma sentencia puede reconocer varias palabras clave y si no consigue establecer alguna relación entre ellas, la respuesta que dará no será satisfactoria. Por suerte, la longitud de las frases que se usan en un Chat normalmente es bastante reducida, ya que la gente prefiere usar varias frases cortas a una larga y elaborada. Al método de las palabras clave, que utiliza un análisis léxico (simplemente se queda con cada una de las palabras del texto de entrada, o con conjunto de ellas, pero sin relacionarlas entre sí) se puede añadir el siguiente paso, un análisis sintáctico (ve la estructura de la frase, intentando averiguar qué papel tienen las palabras que la forman). En frases sencillas, es posible hacerlo, en frases muy complicados es casi imposible. Lo bueno que tienen los chatterbot es que, si ninguno de los análisis que realiza devuelve un resultado satisfactorio, siempre puede salirse por la tangente con alguna frase ingeniosa. Cierto es que, si llevamos a un concurso a nuestro bot, ese tipo de comportamientos se penaliza, pero como inicialmente no creo que ninguno vayamos a competir, nos conformaremos con que resulte medio creíble. Si por último, y posiblemente sea la parte más complicada, logramos que el bot identifique el tema de la conversación que está manteniendo, los resultados pueden ser asombrosos. A favor tenemos que normalmente una conversación que trata sobre un asunto concreto, poco a poco se irá desviando el tema inicial para pasar a otro. Una vez más entre en juego la astucia del programador. Aunque el bot sea incapaz de determinar sobre qué tema le estamos hablando, debería ser capaz de llevar la conversación a algún asunto en concreto sobre el que tenga una colección de frases y respuestas apropiadas. De hecho, cuando se prepara un bot para presentarlo a un concurso, se debe tener en cuenta que le pueden preguntar acerca de cualquier cuestión, con lo que el repertorio de temas del bot, debe ser lo más amplio y variado posible. Para terminar advertiros de que la programación de un chatterbot desde cero no es lo que se dice sencilla, pero, por suerte, se han creado varios lenguajes para programar bots, y están disponibles de forma gratuita en la red. Dichos lenguajes permiten la definición de una serie de esquemas, en los que se pone la palabra clave, o una frase para su reconocimiento y las respuestas que se pueden dar (para una misma frase de entrada, son posibles diferentes respuestas, lo que hace más entretenido y variado al bot). Los lenguajes están lo suficientemente documentados par que, si tenéis interés, podáis hacer vuestro primer bot.

Allan Turing fue uno de los padres de la informática. Nació en 1912 y tiene el típico currículo de inventor renacentista. Fue matemático, filósofo y critógrafo. Trabajó intentando descifrar la máquina ENIGMA en la Segunda Guerra Mundial. Construyó una de las primeras computadores y fue el inventor de la máquina de Turing, un sencillo dispositivo teórico que consta de una cinta sobre la que unas cabezas lectoras y escritoras trabajan. Dicho así no parece nada excepcional, pero resulta que dicha máquina es capaz de ejecutar cualquier programa que exista.

::Más información::

  • Desde www.alicebot.org, la Web de la Fundación de Inteligencia Artificial, podrás chatear con el bot A.L.I.C.E.
  • Si te interesa participar en el nuevo reto de Chatterbox o quieres  ver a los ganadores de ediciones pasadas, visita www.chatterboxchallenge.com
  • Visita www.geocities.com/brizglace y obtendrás una colección de enlaces a Webs sobre chatterbots.

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

 

Tema 6
Metodología de Desarrollo de Programas (II)

 

El objetivo de este Tema es múltiple. En primer lugar se amplía la técnica de refinamientos sucesivos presentada en el Tema 4 con la posibilidad de usar estructuras de selección e iteración.

En segundo lugar se introduce la idea de verificación formal de programas, aunque de una manera muy simplificada, en forma de razonamientos intuitivos sobre las estructuras utilizadas hasta el momento.

Finalmente se trata por primera vez el problema de la eficiencia, y se introduce en forma igualmente simplificada el concepto de complejidad algorítmica.


6.1 Desarrollo por refinamientos usando selección y bucles

En este apartado se ilustra el empleo de la técnica de refinamientos sucesivos explicada en el Tema 4, ampliada ahora con la posibilidad de emplear las nuevas estructuras de selección o bucle. Con ello se tienen tres posibilidades a la hora de refinar una acción compuesta:

  • estructurarla como secuencia de acciones
  • estructurarla como selección de acciones alternativas
  • estructurarla como iteración de acciones

En el Tema 4 se ha presentado ya la metodología para desarrollar esquemas secuenciales. A continuación se amplía la metodología con recomendaciones para desarrollar esquemas de selección y bucles.

6.1.1 Metodología de desarrollo de un esquema de selección
Recordaremos que un esquema de selección consiste en plantear una acción compuesta como la realización de una acción entre varias posibles, dependiendo de ciertas condiciones. Es decir, se trata de elegir para realizar una sola entre varias posibles alternativas.

Para desarrollar un esquema de selección debemos identificar sus elementos componentes. Por tanto habrá que:

a) Identificar cada una de las alternativas del esquema, y las acciones correspondientes.
b) Identificar las condiciones para seleccionar una alternativa u otra.

Como ejemplo, aplicaremos estas recomendaciones al desarrollo de una acción compuesta para calcular cuántos días tiene el mes de Febrero de un cierto año. Reconoceremos que:

a) Las alternativas son que tenga 28 días o que tenga 29. Las acciones serán asignar dicho valor a una variable que almacene el número de días.
          dias := 28      o bien       dias := 29
b) La condición para elegir una acción u otra es que el año sea bisiesto. En forma simplificada (pero válida para años entre 1901 y 2099) expresaremos la condición como equivalente a que el año sea múltiplo de cuatro.
         (anno MOD 4) = 0

Colocando cada elemento identificado en el lugar correspondiente del esquema, tendremos:

IF (anno MOD 4) = 0 THEN
   dias := 29
ELSE
   dias := 28
END

De manera similar se pueden desarrollar esquemas de selección simplificados, con sólo una acción condicionada, o esquemas de selección en cascada en que haya un número más o menos grande de alternativas. Por ejemplo, el esquema anterior podría replantearse realizando primero el tratamiento más común, es decir, que Febrero tenga 28 días, y luego corrigiendo este valor si es necesario.

dias := 28;
IF (anno MOD 4) = 0 THEN
   dias := 29
END

Como ejemplo de selección en cascada, desarrollaremos el cálculo de los días del mes, para cualquier mes del año. Las alternativas son:
31 días: Enero, Marzo, Mayo, Julio, Agosto, Octubre, Diciembre.
30 días: Abril, Junio, Septiembre, Noviembre.
29 días: Febrero (año bisiesto).
28 días: Febrero (año normal).

Para simplificar las expresiones de condición, dejaremos para la última alternativa aquella en que la condición sea más compleja. En este caso sería la de los meses de 31 días, que son los más numerosos. Las otras alternativas podemos situarlas en un orden arbitrario. Al escribir las condiciones debemos tener en cuenta que si hay que evaluar una de ellas es que todas las anteriores han resultado falsas:

IF (mes = 2) AND (anno MOD 4 = 0) THEN
   dias := 29
ELSIF mes=2 THEN
   dias := 28
ELSIF (mes= 4) OR (mes=6) OR (mes=9) OR (mes=11) THEN
   dias := 30
ELSE
   dias := 31
END

6.1.2 Metodología de desarrollo de un esquema de iteración
Una iteración o bucle consiste en la repetición de una acción o grupo de acciones hasta conseguir el resultado deseado. Para desarrollar un esquema de iteración dentro de un programa deberemos identificar cada uno de sus elementos componentes. Al hacerlo hay que identificar simultáneamente las variables adecuadas para almacenar la información necesaria.

En líneas generales se podría proceder de la siguiente manera:

a) Identificar las acciones útiles a repetir, y las variables necesarias. Precisar el significado de estas variables al comienzo y final de cada repetición.
b) Identificar cómo actualizar la información al pasar de cada iteración a la siguiente. Puede ser necesario introducir nuevas variables.
c) Identificar la condición de terminación. Puede ser necesario introducir nuevas variables e incluso acciones adicionales para mantenerlas actualizadas.
d) Identificar los valores iniciales de las variables, y si es necesaria alguna acción para asignárselos antes de entrar en el bucle.

Además de los elementos anteriores, puede ser necesaria alguna acción adicional al comienzo o al final del bucle. Si son acciones muy simples, pueden considerarse parte del esquema de iteración como tal. Si son algo complejas será mejor considerarlas como acciones anteriores o posteriores, siguiendo un esquema secuencial.

El método explicado corresponde al caso general. En bastantes casos el desarrollo de un bucle es mucho más sencillo. Esto ocurre en particular cuando se programan bucles con contador, si las acciones a repetir pueden expresarse directamente en función del contador del bucle. En este caso los pasos b), c) y d) se refunden en uno sólo, consistente en determinar los valores inicial, final y el incremento del contador del bucle.

Desarrollaremos con la técnica indicada un fragmento de programa que imprima los términos de la serie de Fibonacci. Cada término de esta serie se obtiene sumando los dos anteriores. La serie comienza con los términos 0 y 1, que se suponen ya impresos antes del bucle. Se trata de calcular e imprimir tantos términos como sea posible.

Procediendo paso a paso, describiremos cada elemento del desarrollo de manera informal, seguido de su codificación en Modula-2.

a) Acciones útiles a repetir: Imprimir un término.
         WriteInt( termino, 10); WriteLn
   Variables necesarias: El término a imprimir.
         termino: INTEGER;
   Valor al empezar la repetición: Último término impreso hasta el momento.

b) Actualizar las variables al pasar de una repetición a la siguiente:
   Antes de imprimir, calcular el término actual a partir de los dos anteriores (se necesita tener almacenado el penúltimo término).
         aux := termino + anterior;
         anterior := termino;
         termino := aux
   Variables adicionales: El penúltimo término, y una variable temporal.
         anterior: INTEGER;
         aux: INTEGER;

c) Condición de terminación: El término siguiente excedería del rango de los enteros. Hay que evaluar la condición sin calcular explícitamente el valor de dicho término, porque se produciría "overflow".
         MAX(INTEGER)-termino < anterior
(Obsérvese que esta expresión equivale a MAX(INTEGER) < termino + anterior)

d) Valores iniciales de las variables: Los dos primeros términos, 0 y 1.
         anterior := 0;
         termino := 1

El bucle completo sería:

VAR
   termino, anterior, aux: INTEGER;
….

anterior := 0;
termino := 1;
WHILE MAX(INTEGER)-termino >= anterior DO
   aux := termino + anterior;
   anterior := termino;
   termino := aux;
   WriteInt( termino, 10 );
   WriteLN
END


6.2 Ejemplos de desarrollo de programas

A continuación se ilustra la técnica de desarrollo por refinamientos sobre algunos ejemplos típicos.

6.2.1 Ejemplo: Imprimir el borde de un triángulo
Se trata de imprimir con asteriscos el perímetro de un triángulo aproximadamente equilátero, tal como se indica a continuación:

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

Como parámetro del problema se especificará la altura del triángulo. Si la altura es N, habrá que imprimir N líneas, cada una con la configuración de asteriscos que corresponda. El triángulo anterior tiene altura 7. Se pretende además que el triángulo aparezca ajustado a la izquierda; es decir, el primer asterisco de la línea inferior deberá empezar exactamente en la primera posición de esa línea. Las otras líneas empezarán con el espacio en blanco apropiado.

El planteamiento inicial del programa será:

     Imprimir el borde de un triángulo

El primer paso de refinamiento consistirá en decidir si esta acción compuesta debe plantearse como una secuencia de acciones simples, o como selección entre alternativas, o como bucle. Observando que hay tres clases de líneas a imprimir, según el número de asteriscos que contienen, elegiremos la primera opción, y escribiremos:

     Imprimir el borde de un triángulo —>
          Imprimir el vértice superior
          Imprimir los bordes laterales
          Imprimir el borde inferior

El orden en que deben realizarse las acciones viene determinado por el hecho de que la impresora va imprimiendo las líneas sucesivas de arriba a abajo.

A continuación refinaremos cada una de estas acciones. Para ello examinaremos en detalle los caracteres que hay que imprimir en cada línea, en función de la altura del triángulo. Marcando con puntos los espacios en blanco, tendremos:

Línea 1   ……*         N-1 blancos
Línea 2   …..*.*        N-2 blancos, 1 blanco
Línea 3   ….*…*       N-3 blancos, 3 blancos
          …*…..*   
Línea K   ..*…….*     N-k blancos, 2k-3 blancos
          .*………*
Línea N   *.*.*.*.*.*.*   N asteristos espaciados

El refinamiento de la primera acción será en forma de secuencia:

         Imprimir el vértice superior —>
              Imprimir N-1 blancos
              Imprimir un asterisco
              Saltar a la línea siguiente

La acción de imprimir los blancos será una iteración, que podemos escribir directamente mediante un bucle con contador:

          Imprimir N-1 blancos —>
               FOR k := 1 TO N-1 DO
                  Write( ‘ ‘ )
               END

Las acciones de imprimir un asterisco y saltar a la línea siguiente se escriben inmediatamente:

         Imprimir un asterisco —>
              Write( ‘*’ )

         Saltar a la línea siguiente —>
              WriteLn

En el siguiente paso de refinamiento detallaremos la impresión de los bordes laterales. Intuitivamente podemos establecer una estructura de iteración mediante un bucle con contador:

         Imprimir los bordes laterales —>
              FOR k := 2 TO N-1 DO
                 Imprimir los bordes de la línea k
              END

Observaremos que la línea k-esima va precedida de N-k blancos, y tiene 2k-3 blancos entre los bordes:

          Imprimir los bordes de la línea k —>
                  Imprimir N-k blancos
                  Imprimir un asterisco
                  Imprimir 2k-3 blancos
                  Imprimir un asterisco
                  Saltar a la línea siguiente

Todas estas acciones han sido ya refinadas en sentencias de Modula-2. Finalmente falta por plantear la impresión del borde inferior. Podremos distinguir la impresión del primer asterisco y la del resto, y escribir:

           Imprimir el borde inferior —>
                 Imprimir un asterisco
                 Imprimir N-1 asteriscos precedidos de blanco
                 Saltar a la línea siguiente

La acción central se escribirá como un bucle con contador:

          Imprimir N-1 asteriscos precedidos de blanco —>
                FOR k := 1 TO N-1 DO
                   Write( ‘ ‘ ); Write( ‘*’ )
                END

Reuniendo todos los fragmentos, añadiendo la parte declarativa necesaria, y documentando cada elemento importante del programa, tendremos el programa completo, tal como se recoge en el listado que se muestra a continuación.

Este programa puede compilarse y ejecutarse, obteniendo el resultado puesto anteriormente como ejemplo. Sin embargo no puede considerarse totalmente terminado. Una deficiencia es que la altura del triángulo aparece como constante, cuando en realidad sería más razonable leerla como dato en cada ejecución del programa. La solución será definir N como variable y leer su valor al comienzo.

Otra deficiencia es que no se tienen en cuenta algunos casos especiales, en que el triángulo degenera y no tiene todos los elementos identificados. Esto ocurre cuando la altura del triángulo es 0, 1 ó 2. En el primer caso se omiten todas las líneas; en el segundo sólo existe el vértice superior, y en el tercero no existen bordes laterales.

(******************************************************************
*
*    Programa: Triángulo (Versión inicial)
*
*    Descripción:
*      Este programa imprime el borde de un triángulo
*      aproximadamente equilátero, usando asteriscos.
*
******************************************************************)
MODULE Triangulo;

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

(*=================================================================
    PARÁMETROS GENERALES   
=================================================================*)
  CONST N = 7;        (* altura del triángulo *)
  VAR j, K: INTEGER;    (* contadores *)

BEGIN

(*=================================================================
    PARTE EJECUTABLE LE PROGRAMA
=================================================================*)
  (*– Imprimir el vértice superior –*)
    FOR K := 1 TO N-1 DO
      Write( ‘ ‘ )
    END;
    Write( ‘*’ );
    WriteLn;

  (*– Imprimir los bordes laterales –*)
    FOR K := 2 TO N-1 DO
      FOR j := 1 TO N-K DO
        Write( ‘ ‘ )
      END;
      Write( ‘*’ );
      FOR j := 1 TO 2*K-3 DO
        Write( ‘ ‘ )
      END;
      Write( ‘*’ );
      WriteLn
    END;

  (*– Imprimir el borde inferior –*)
    Write( ‘*’ );
    FOR K := 1 TO N-1 DO
      Write( ‘ ‘ ); Write( ‘*’ )
    END;
    WriteLn
END Triangulo.

En esta versión inicial, para la altura igual a 2 no se obtienen resultados erróneos, ya que el bucle de líneas de los bordes laterales tal como está escrito se ejecutaría 0 veces (pero esto se debe más a la casualidad que a un razonamiento previo del comportamiento del programa). Por ejemplo, si cambiásemos el parámetro de altura al valor 1:

              CONST N = 1;

obtendríamos el resultado erróneo:

*
*

este mismo resultado se obtiene para N = 0. La solución general será convertir en esquema condicional la impresión del vértice superior y del borde inferior:

           Imprimir el vértice superior —>
                IF N>0 THEN
                     Imprimir N-1 blancos
                     Imprimir un asterisco
                     Saltar a la línea siguiente

                END

            Imprimir el borde inferior —>
                IF N>1 THEN
                     Imprimir un asterisco
                     Imprimir N-1 asteriscos precedidos de blanco
      
          END

La versión corregida del programa aparece en el siguiente listado:

(***********************************************************************
*
*    Programa: Triangulo (Versión corregia)
*
*    Descripción:
*      Este programa imprime el borde de un triángulo
*      aproximadamente equilátero, usando asteriscos.
*      La altura del triángulo, en líneas de texto, se lee como dato.
*
***********************************************************************)
MODULE Triangulo;

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

  VAR N: INTEGER;        (* altura del triángulo *)
  VAR j, k: INTEGER;        (* contadores *)

BEGIN

(*======================================================================
    PARTE EJECUTABLE DEL PROGRAMA
======================================================================*)
  (*– Leer la altura deseada –*)
    WriteString( "Altura: " );
    ReadInt( N );
    WriteLn;

  (*– Imprimir el vértice superior –*)
    IF N>0 THEN
      FOR k := 1 TO N-1 DO
        Write( ‘ ‘ )
      END;
      Write( ‘*’ );
      WriteLn
    END;

  (*– Imprimir los bordes laterales –*)
    FOR k := 2 TO N-1 DO
      FOR j:= 1 TO N-k DO
        Write( ‘ ‘ )
      END;
      Write( ‘*’ );
      FOR j := 1 TO 2*k-3 DO
        Write( ‘ ‘ )
      END;
      Write( ‘*’ );
      WriteLn
    END;

  (*– Imprimir el borde inferior –*)
    IF N>1 THEN
      Write( ‘*’ );
      FOR k := 1 TO N-1 DO
        Write( ‘ ‘ ); Write( ‘*’ )
      END;
      WriteLn
    END
END Triangulo.
   

Algunos ejemplos de ejecución serian los siguientes:

Altura: 7
      *
     * *
    *   *
   *     *
  *       *
 *         *
* * * * * * *

Altura: 1
*

6.2.2 Ejemplo: Imprimir el triángulo de Floyd
Se trata de desarrollar un programa que imprima el llamado triángulo de Floyd. Este "triángulo" se forma imprimiendo los sucesivos números naturales 1, 2, 3, … en filas sucesivas, colocando 1 número en la primera línea, 2 en la segunda, 3 en la tercera, etc. Si fijamos un límite arbitrario de la serie de números (p. ej. 12), tendremos el triángulo

              1
              2     3
              4     5     6
              7     8     9     10
              11   12

Es fácil darse cuenta de que en general la línea k tiene k números, excepto la última línea, que puede quedar incompleta. El programa de este ejemplo deberá leer como dato el límite de la serie.

El primer paso de refinamiento será:

            Imprimir el triángulo de Floyd —>
                     Leer el límite N de la serie
                     Imprimir el triángulo hasta N

La primera acción puede desarrollarse en sentencias de Modula-2 en forma inmediata:

           Leer el límite N de la serie —>
                     WriteString( "Límite de la serie: ");
                     ReadInt( N );
                     WriteLn;

La parte principal del programa es la impresión del triángulo. Habremos de refinarla en forma de un esquema de secuencia, alternativa o iteración. Puesto que se trata de imprimir un número variable de líneas y valores, con acciones similares para todos ellos, parece razonable usar un esquema de iteración. La decisión a tomar será si plantear la iteración como un bucle de números o de líneas. Según la elección que hagamos tendremos esquemas de programa diferentes, que desarrollaremos por separado.

6.2.2.1 Escritura mediante bucle de números
Elegiremos en primer lugar esta posibilidad, que facilita la codificación del bucle, ya que puede plantearse como un bucle con contador:

               Imprimir el triángulo hasta N —>
                    FOR k := 1 TO N DO
                        imprimir el número k
                    END

La impresión del número requiere acciones adicionales, ya que debe incluir el saltar de línea al comienzo de cada nueva línea del triángulo. Puede plantearse como secuencia de acciones, en la forma:

                imprimir el número k —>
                      saltar de línea, si es necesario
                      WriteInt( k, 5 );

En este planteamiento los saltos de línea no se producen en cuanto se llega al final de la línea, sino cuando se va a escribir el siguiente valor. Esto quiere decir que el último número impreso dentro del bucle no irá seguido de salto de línea, y que habrá que completar la última línea como acción de terminación, fuera del bucle; es decir, la escritura del triángulo terminará con una acción adicional:

                 Completar la última línea

Refinaremos la acción de saltar de línea en forma de esquema condicional:

                  saltar de línea, si es necesario —>
                      IF el número anterior fue el último de su línea THEN
                         WriteLn
                      END

El refinamiento de la condición va a exigir introducir nuevos elementos. Para expresar dicha condición como una expresión en Modula-2 necesitaremos mantener actualizada la indicación de cuál es el último valor de cada línea. En todo caso necesitaremos conocer cuántos números han de imprimirse en la línea en curso (recordemos que en la línea k habrá k números).

Usaremos una variable linea como contador de líneas, y otra variable ultimo para contener el último número de la línea. Estas variables habrán de actualizarse cada vez que se cambie de línea. Reescribiremos el refinamiento anterior en la forma:

                  saltar de línea, si es necesario —>
                      IF k>ultimo THEN
                         WriteLn;
                          linea := linea+1;
                          ultimo := ultimo + linea
                      END

Al mismo tiempo hay que ampliar la inicialización del bucle de escribir números incluyendo dar valores iniciales a las nuevas variables, en concreto:

                   linea := 1;
                   ultimo := 1;

Esta inicialización es la adecuada incluso en el caso de escribir 0 números, ya que los valores corresponden al primer número que completa línea, y tras el cual habrá que saltar de línea si se imprimen más números.

No hay que olvidar desarrollar la acción de saltar de línea al final de la última línea del triángulo. Esta acción será condicional si hemos de considerar el caso de que se manden escribir 0 números, en cuyo caso no existe esa última línea.

                  Completar la última línea —>
                     IF N>0 THEN WriteLn END

Con todo ello podemos ya redactar el programa Floyd1 completo, tal como aparece a continuación, documentándolo en la forma habitual.

(**********************************************************************************************
*
*    Programa: Floyd1
*
*    Descripción:
*      Este programa imprime el triángulo de Floyd con los
*      números correlativos de 1 a N.
*      El valor de N se lee como dato.
*
**********************************************************************************************)
MODULE Floyd1;

(*=============================================================================================
    IMPORTACIÓN Y DECLARACIONES DEL PROGRAMA
=============================================================================================*)
  FROM InOut IMPORT WriteLn, WriteString, ReadInt, WriteInt;
  VAR
    N: INTEGER;            (* último número a imprimir *)
    k: INTEGER;            (* contador de números *)
    linea: INTEGER;        (* contador de líneas *)
    ultimo: INTEGER;        (* último número de la línea *)

BEGIN

(*=============================================================================================
    PARTE EJECUTABLE DEL PROGRAMA
=============================================================================================*)
  (*– leer el límite de la serie –*)
    WriteString( "Límite de la serie: " );
    ReadInt( N );
    WriteLn;

  (*– imprimir el triángulo mediante un bucle de número –*)
    linea := 1;
    ultimo := 1;
    FOR k := 1 TO N DO
      (*– imprimir el número k –*)
         (*– saltar de línea, si es necesario –*)
       IF k>ultimo THEN
         WriteLn;
         linea := linea + 1;
         ultimo := ultimo + linea
       END;
        WriteInt( k,5 )            (* imprimir el número *)
    END;
    IF N>0 THEN WriteLn END        (* acabar última línea *)
END Floyd1.

Un ejemplo de la ejecución del programa sería la siguiente:

Límite de la serie: 12
    1
    2    3
    4    5    6
    7    8    9   10
   11   12