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: 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.