25 KiB
PR 2
Volver a la página principal de "Fundamentos de la Programación"
Índice
- PR 2
- 12. Modularidad
- 12.1 Aproximación intuitiva
- 12.2 Funciones
- 12.3. Acciones
- 12.4. Cómo utilizar (llamar) a una función y/o acción
- 12.5. Clases de parámetros de una acción
- 12.6. Funciones contra acciones
- 12.7. Acciones y funciones predefinidas
- 12.8. Modularización
- 12.9 Ejercicios
- 12.10 Soluciones en lenguaje algorítmico
- 12. Modularidad
12. Modularidad
La modularidad es una técnica de programación que consiste en dividir un programa en partes más pequeñas y manejables. Cada parte se denomina módulo y puede ser un subprograma, una función, una acción, un procedimiento, una rutina, un método, etc. La modularidad permite dividir un problema complejo en problemas más simples y fáciles de resolver. Además, facilita la reutilización de código, la depuración y el mantenimiento de los programas.
12.1 Aproximación intuitiva
Un ejemplo con una comida:
algorithm almuerzo
var
primerPlato, salsa, segundoPlato, postres: tPlato;
end var
primerPlato:= encargarPaella(marisco, 5);
segundoPlato:= encargarPescado(bacalao, ajoaceite, 5);
postres:= encargarPostres(pastel, chocolate, velas, 5);
end algorithm
Podemos ver cómo nuestro algoritmo, a base de solicitar a expertos que hagan el trabajo, se convierte en un algoritmo simple y fácil de entender. Lo único que hemos tenido que hacer son peticiones en las que hemos añadido valores, a los que llamamos parámetros, que indican exactamente cómo queremos cada plato.
Los lenguajes de programación permiten trabajar de una forma similar a como lo hemos hecho en el menú. Es decir, se pueden utilizar subalgortimos, o se pueden definir otros nuevos, que quedan aparte del algoritmo principal y que permiten dividir un algoritmo complicado en subalgoritmos más simples. Además, como veremos en los siguientes apartados, otra de las ventajas que proporcionan es el hecho de que estos subalgoritmos se pueden reutilizar evitando la reescritura de código.
Estos subalgoritmos reciben el nombre de acciones y funciones y es como si empaquetásemos un conjunto de instrucciones que resuelven un problema y no nos tenemos que preocupar de lo que hay dentro.
12.1.1. Ejemplo - Cálculo de combinaciones
Supongamos que tenemos que diseñar un algoritmo que permita saber cuántas combinaciones de colores se pueden hacer si disponemos de n
telas, cada una de un color diferente. Concretamente se quiere disponer de un algoritmo genérico que dados n
colores calcule cuantas combinaciones se pueden hacer utilizando cada vez m
colores, donde m
y n
son enteros y m ≤ n
.
Para diseñar el algoritmo deberemos leer los valores de n y m y haberlo indicado en la fórmula. Quedaría:
algorithm combinations
var
{definimos las variables, dos enteros que hay que leer y una donde almacenar el resultado}
totalNumColors, colorsToCombine: integer ;
result: integer;
end var
{leemos los dos números}
totalNumColors:= readInteger();
colorsToCombine:= readInteger();
{Hacemos un esbozo del cálculo, suponiendo que disponemos de una manera de calcular el factorial de un número }
result := totalNumColors! div (colorsToCombine! * (totalNumColors-colorsToCombine)!) ;
writeInteger(result);
end algorithm
#include <stdio.h>
int main() {
int totalNumColors;
int colorsToCombine;
int result;
result = totalNumColors! / (colorsToCombine! * (totalNumColors-colorsToCombine)!) ;
printf ("%d ", result);
return 0;
}
Ahora bien, para calcular el factorial de las posible combinaciones se necesita hacer una iteración para ir calculando el producto de todos los enteros, además de la necesidad de variables donde guardar los cálculos parciales::
var
factorial: integer;
i: integer;
n: integer;
end var
n:= readInteger();
factorial:= 1;
i:=1;
while i ≤ n do
factorial:=factorial*i;
i:=i+1;
end while
#include <stdio.h>
int main() {
int factorial;
int i;
int n;
scanf("%d", &n);
factorial=1;
i=1;
while(i <= n) {
factorial = factorial*i;
i++;
}
return 0;
}
Si incorporamos este código en el algoritmo anterior, el código se complica y se hace menos legible. Deberíamos repetir el código cada vez que necesitaramos el cálculo. La solución es encapsular este código en una acción o función que se encargue de calcular el factorial de un número cada vez que se necesite con una llamada que pase el parámetro. De esta forma, el algoritmo principal quedaría más limpio y fácil de entender.
12.2 Funciones
Una función es un conjunto independiente de instrucciones que resuelven una tarea concreta, que devuelve un valor y que puede ser referenciada (invocada o llamada) desde otro punto del programa.
12.2.1. Sintaxis para la declaración de una función
Para definir una función necesitaremos los siguientes elementos:
- Nombre para poder identificarla.
- Una serie de valores sobre los que se quieran hacer los cálculos, que se denominan parámetros formales de la función. Es necesario indicar el nombre y el tipo de dato de cada uno de los parámetros. Actúan como variables locales a la función.
- Las instrucciones de la función, es decir, el cuerpo de la función.
- El valor resultante de la función al que se debe indicar el tipo de dato que se va a devolver. Este valor se devuelve mediante la instrucción
return
.
Sintaxis:
function name(param1: type1 , param2: type2, ..., paramn: typen): returnType
...
...
return expression;
end function
returnType name(type1 param1, type2 param2, ..., typen paramn) {
...
...
/* returnValue es el valor que calcula la función */
return returnValue;
}
La cabecera es la parte que describe cómo debe llamarse a una función. Indica su nombre, los parámetros que espera y el tipo que retorna. La estructura de la cabecera es la siguiente:
function name(param1: type1 , param2: type2, ..., paramn: typen): returntype;
returnType name(type1 param1, type2 param2, ..., typen param);
Volviendo a nuestro ejemplo del factorial, la función sería:
function factorial (number: integer): integer
var
fact: integer;
i: integer;
end var
fact:= 1;
i:=1;
while i ≤ number do
fact:= fact*i;
i:= i+1;
end while
return fact;
end function
#include <stdio.h>
int factorial(int number) {
int fact;
int i;
fact=1;
i=1;
while(i <= number) {
fact = fact*i;
i++;
}
return fact;
}
Aquí vemos:
- El nombre factorial para identificar la función.
- Un único parámetro, en este caso un entero, para identificar el número sobre el que queremos hacer el cálculo del factorial.
- El tipo de retorno, que también será entero.
- El cuerpo de la función, que hace los cálculos.
Recordemos que las variables son locales a la función, es decir, que no se pueden utilizar fuera de la función.
12.3. Acciones
A veces interesa un subalgoritmo que realice una serie de instrucciones sin devolver un valor. En estos casos, el lenguaje algorítmico generaliza el concepto de función y define la acción.
Una acción es un conjunto de instrucciones que resuelven una tarea concreta y que puede ser referenciada (invocada o llamada). Se diferencia de una función porque:
- No devuelve ningún valor.
- Cuando modifica un parámetro formal, puede estar también modificando el parámetro real.
- Se debe indicar delante del nombre del parámetro cómo se pasa. Utilizamos las palabras clave
in
,out
oinout
.
12.3.1. Sintaxis para la declaración de una función
Para definir una acción necesitaremos los siguientes elementos:
- Nombre para poder identificarla.
- Una serie de valores sobre los que se quieran hacer los cálculos, que se denominan parámetros formales de la acción. Es necesario indicar el nombre y el tipo de dato de cada uno de los parámetros. Actúan como variables locales a la acción.
- Las instrucciones de la acción, es decir, el cuerpo de la acción.
Sintaxis:
action name(class1 param1: type1 , class2 param2: type2, ..., classn paramn: typen)
...
...
end action
void name(type1 param1, type2 param2, ..., typen paramn) {
...
...
}
12.4. Cómo utilizar (llamar) a una función y/o acción
Una vez definida la acción o función esta se puede utilizar desde el algoritmo principal o desde otra función o desde otra acción.
Para llamar a una función o acción se utiliza el nombre de la función o acción seguido de los parámetros que se le pasan entre paréntesis. Los parámetros reales pueden ser variables, constantes o expresiones, que deben coincidir en número, tipo y orden con los parámetros formales.
12.4.1. Llamada a una función
Ejemplo:
{Si suponemos que tenemos la siguiente cabecera}
function name(param1: integer , param2: real, param3: char): integer;
{y suponemos que tenemos la declaración de las siguientes variables}
var
a: integer;
b: real;
c: char;
result: integer;
end var
{La manera de llamar a la función sería:}
result := name(a, b, c);
{Las variables a, b y c son los parámetros actuales de la función}
//Si suponemos que tenemos la siguiente cabecera:
int name(int param1, float param2, char param3);
//Y suponemos que tenemos la declaración de las siguientes variables:
int a;
float b;
char c;
int result;
//La manera de llamar a la función en C sería:
result = name(a, b, c);
//Las variables a, b y c son los parámetros actuales de la función
En el caso del factorial, la cabecera era:
function factorial (number: integer): integer;
Las posibles maneras de llamar a la función serían:
n:= factorial(5); /* utilizando una constante como parámetro actual*/
n:= factorial(p) /* utilizando una variable como parámetro actual */
n:= factorial(m+2) /* utilizando una expresión como parámetro actual */
Ahora ya podemos ver el algoritmo completo del factorial:
function factorial (number: integer): integer
var
fact: integer;
i: integer;
end var
fact:= 1;
i:=1;
while i ≤ number do
fact:=fact*i;
i:=i+1;
end while
return fact;
end function
algorithm combinations
var
{definimos las variables, dos enteros que hay que leer y una donde almacenar el resultado }
totalNumColors, colorsToCombine: integer ;
result: integer;
end var
{leemos los números}
totalNumColors:= readInteger();
colorsToCombine:= readInteger();
result := factorial(totalNumColors) div ( factorial(colorsToCombine) * factorial(totalNumColors-colorsToCombine));
writeInteger(result);
end algorithm
#include <stdio.h>
int factorial(int number) {
int fact;
int i;
fact = 1;
i = 1;
while(i <= number) {
fact = fact*i;
i++;
}
return fact;
}
int main() {
int totalNumColors;
int colorsToCombine;
int result;
scanf("%d", &totalNumColors);
scanf("%d", &colorsToCombine);
result = factorial(totalNumColors) / ( factorial(colorsToCombine) * factorial(totalNumColors-colorsToCombine));
printf ("%d ", result);
return 0;
}
12.4.2. Llamada a una acción
Ejemplo:
{Si suponemos que tenemos la siguiente cabecera de una acción con todos los parámetros de entrada}
action name(in param1: integer , in param2: real, in param3: char);
{y suponemos que tenemos la declaración de las siguientes variables}
var
a: integer;
b: real;
c: char;
end var
{La manera de llamar a la acción sería:}
name(a, b, c);
{Las variables a, b y c son los parámetros actuales de la acción}
//Si suponemos que tenemos la siguiente cabecera:
void name(int param1, float param2, char param3);
//Y suponemos que tenemos la declaración de las siguientes variables:
int a;
float b;
char c;
//La manera de llamar a la acción en C sería:
name(a, b, c);
//Las variables a, b y c son los parámetros actuales de la acción
12.5. Clases de parámetros de una acción
Los parámetros de una acción pueden ser de tres tipos:
- Parámetro de entrada (in). El valor del parámetro solamente será consultado y utilizado dentro de la acción. Esta manera de pasar parámetros es típica de funciones matemáticas y es la única manera que puede usarse en las funciones.
- Parámetro de salida (out). Se utiliza para asignar un valor al parámetro. Si el parámetro ya tiene un valor inicial, este valor no se debería utilizar dentro de la acción. Normalmente se pasan los parámetros de esta manera para que sean inicializados por la acción. Puesto que la acción dejará un valor en el parámetro, este debe ser obligatoriamente una variable.
- Parámetro de entrada/salida (inout). Se utilizan para actualizar el valor del parámetro. En este caso, el valor inicial del parámetro se puede utilizar y también se puede modificar, devolviendo un nuevo valor a quien ha llamado a la acción. Puesto que la acción dejará un nuevo valor en el parámetro, este debe ser obligatoriamente una variable.
12.5.1. Parámetros de entrada
Disponemos de una acción que, dados el número de piezas compradas a un mayorista y el precio por pieza, calcula el coste total y lo muestra por el canal estándar, teniendo en cuenta que según el número de piezas, se aplicará un tipo de descuento u otro:
const
MAX_DISCOUNT: real = 0.15;
MIN_DISCOUNT: real = 0.05;
LIMIT: integer = 1000;
end const
action computeCost(in num: integer, in price: real)
var
val: real;
discount: real;
end var
if num ≥ LIMIT then
discount:= MAX_DISCOUNT;
else
discount:= MIN_DISCOUNT;
end if
val:= integerToReal(num) * price * (1.0 - discount);
writeReal(val);
end action
algorithm example1
var
num: integer;
price: real;
end var
num:= 1005;
price:= 10.0;
computeCost(num, price);
end algorithm
#include <stdio.h>
#define MAX_DISCOUNT 0.15
#define MIN_DISCOUNT 0.05
#define LIMIT 1000
void computeCost(int num, float price) {
float val;
float discount;
if (num >= LIMIT) {
discount = MAX_DISCOUNT;
} else {
discount = MIN_DISCOUNT;
}
val = (float) (num)*price* (1.0 - discount);
printf("%f", val);
}
int main() {
float price;
int num;
num = 1005;
price = 10.0;
computeCost(num, price);
return 0;
}
Comentarios:
- Aunque los parámetros formales y actuales tengan el mismo nombre, se trata de variables diferentes.
- Podemos ver que los parámetros actuales no han cambiado.
12.5.2. Parámetros de salida
El ejemplo anterior pero con una acción que tiene un parámetro de salida para devolver el coste:
const
MAX_DISCOUNT: real = 0.15;
MIN_DISCOUNT: real = 0.05;
LIMIT: integer = 1000;
end const
action computeCost(in num: integer, in price: real, out val: real)
var
discount: real;
end var
if num ≥ LIMIT then
discount:= MAX_DISCOUNT;
else
discount:= MIN_DISCOUNT;
end if
val:= integerToReal(num) * price * (1.0 - discount);
end action
algorithm example2
var
num: integer;
price: real;
cost: real;
end var
num:= 1005;
price:= 10.0;
computeCost(num, price, cost);
writeReal(cost);
end algorithm
#include <stdio.h>
#define MAX_DISCOUNT 0.15
#define MIN_DISCOUNT 0.05
#define LIMIT 1000
void computeCost(int num, float price, float *val) {
float discount;
if (num >= LIMIT) {
discount = MAX_DISCOUNT;
} else {
discount = MIN_DISCOUNT;
}
*val = (float) (num)*price* (1.0 - discount);
}
int main() {
float price;
int num;
float cost;
num = 1005;
price = 10.0;
computeCost(num, price, &cost);
printf("%f", cost);
return 0;
}
Comentarios:
- Al tratarse de una acción, se llama sin asignar el retorno a una variable, ya que las acciones no retornan ningún valor.
- La acción tiene un parámetro formal de salida. Esto quiere decir que cada vez que se modifique este parámetro dentro de la acción, automáticamente se modificará con el mismo valor el parámetro actual correspondiente cost.
- Podemos ver que en lenguaje C, los parámetros de salida son en realidad Punteros. Más adelante se muestra un ejemplo.
12.5.3. Parámetros de entrada/salida
Supongamos ahora que el coste final es la suma de unos gastos mínimos (coste de entrega, etc.) más el coste de venta. El coste mínimo viene dado por una constante BASIC_COST.
const
MAX_DISCOUNT: real = 0.15;
MIN_DISCOUNT: real = 0.05;
LIMIT: integer = 1000;
BASIC_COST: real = 100.0;
end const
action computeCost(in num: integer, in price: real, inout val: real)
var
discount: real;
end var
if num ≥ LIMIT then
discount:= MAX_DISCOUNT;
else
discount:= MIN_DISCOUNT;
end if
val:= val + integerToReal(num) * price * (1.0 - discount);
end action
algorithm example3
var
num: integer;
price: real;
cost: real;
end var
num:= 1005;
price:= 10.0;
cost:= BASIC_COST;
computeCost(num, price, cost);
writeReal(cost);
end algorithm
#include <stdio.h>
#define MAX_DISCOUNT 0.15
#define MIN_DISCOUNT 0.05
#define LIMIT 1000
#define BASIC_COST 100.0
void computeCost(int num, float price, float *val) {
float discount;
if (num >= LIMIT) {
discount = MAX_DISCOUNT;
} else {
discount = MIN_DISCOUNT;
}
*val = *val + (float) (num) * price * (1.0 - discount);
}
int main() {
float price;
int num;
float cost;
num = 1005;
price = 10.0;
cost = BASIC_COST;
computeCost(num, price, &cost);
printf("%f", cost);
return 0;
}
Comentarios:
- La única diferencia con el ejemplo 2 es que el parámetro val ahora es de entrada y salida y su valor se actualiza sumándole el coste de venta.
12.6. Funciones contra acciones
Función: el criterio para usar una función es evaluar si esta puede actuar como una función matemática que, dados unos parámetros, puede retornar un valor concreto que se puede utilizar como un miembro de una expresión. Si es así, claramente debemos escribir una función.
Acción: si necesitamos actualizar uno de los parámetros o inicializar uno que no lo está, claramente debemos usar una acción. También se utiliza cuando no se retorna ningún valor o cuando se deben devolver más de uno.
Muchas veces las acciones tienen algunos parámetros de salida, pero se podría dar el caso de que no tenga ninguno como, por ejemplo, en el caso de que la acción lea la información del canal estándar y saque también los resultados directamente por el canal estándar.
12.7. Acciones y funciones predefinidas
Tanto en el lenguaje algorítmico como en los de programación, hay algunas acciones y funciones ya definidas por defecto.
Entre otras tenemos:
- Funciones de conversión de tipo.
- Acciones y funciones de entrada y salida de datos.
Las funciones de conversión de tipo son esas que transforman una variable de un tipo a otro:
function integerToReal(x: integer): real;
function realToInteger(x: real): integer;
function charToCode(c: char): integer;
function codeToChar(x: integer): char;
#include <stdio.h>
int main() {
int x;
float y;
char c;
y = (float) x;
x = (int) y;
x = (int) c;
c = (char) x;
}
Las acciones/funciones de entrada y salida de datos son esas que permiten recibir datos del exterior (teclado, ficheros, etc.) y mostrar datos al exterior:
function readInteger(): integer;
function readReal(): real;
function readChar(): char;
function readString(): string;
action writeInteger(in x: integer);
action writeReal(in x: real);
action writeChar(in x: char);
action writeString(in x: string);
#include <stdio.h>
int main() {
int x;
float y;
char c;
string s;
scanf("%d", &x);
scanf("%f", &y);
scanf("%c", &c);
scanf("%s", s);
printf("%d ", x);
printf("%f ", y);
printf("%c ", c);
printf("%s ", s);
return 0;
}
12.8. Modularización
Para mantener el conjunto de acciones y funciones ordenadas y que se puedan utilizar en diferentes programas de manera fácil, los lenguajes permiten crear librerías.
Estas librerías constan de dos partes:
- Un fichero de texto que inclute las cabeceras de las acciones y funciones que la componen y que en C tienen siempre la extensión
.h
. - El código de las acciones y funciones, que también tendrá un extensión
.c
, pero que solamente tendrá el código de la acciones y/o funciones, sin tener unmain
.
Cuando se tendan que emplear desde algún otro programa solamente se incluirán en el programa el fichero de cabeceras.h. La sintaxis:
#include "nomLlibreria.h"
Concretamente, la librería que incluye las cabeceras de las acciones y funciones de entrada y salida de datos se llama en C stdio.h
:
#include <stdio.h>
Hay muchos ejemplos de librerías, por ejemplo:
- Librerías de funciones matemáticas.
math.h
,stdlib.h
, etc. - Librerías de funciones y acciones de tratamiento de fechas.
time.h
,date.h
, etc. - Librerías de funciones de tratamiento de cadenas de caracteres.
string.h
, etc.
La posibilidad de crear las librerías facilita la reutilización del código y estandarización de la manera de trabajar.
12.9 Ejercicios
- Diseñad una función que, dado un entero n, calcule la suma de los primeros n números de la serie Fibonacci.
- Diseñad una acción que, dados un real que representa el saldo de una cuenta corriente y otro real que representa un cargo o imposición, actualice el saldo de la cuenta.
- Diseñad una función que calcule n (real) elevado a la potencia m (entero).
- Diseñad una acción que, dados tres números reales que representan los coeficientes de una ecuación de segundo grado, retorne sus raíces, en caso de que existan. En caso contrario, que inicialice las raíces a 0. Recordad que dada la ecuación A * x2 + B * x + C las raíces se calculan:
x1 = (-B + sqrt(B2 - 4 * A * C)) / (2 * A)
Si la expresión dentro de la raíz es negativa quiere decir que la ecuación no tiene raíces. Podéis suponer que la función raíz cuadrada está ya definida y su cabecera es:
function squareRoot(x: real): real;
4b. Declarad las variables necesarias para llamar a la acción del ejercicio anterior e indicad cómo se llamaría.
- Para cada apartado siguiente, decidid si es mejor una acción o una función y definid su cabecera (solamente se pide la cabecera, no hay que diseñar ningún algoritmo).
- Dadas las dimensiones de una pared (altura y anchura) y el precio de pintura por metro cuadrado, determinad el coste total de pintura necesario para pintar la pared.
- Dado el consumo total de agua acumulado desde el mes de enero hasta junio (ambos incluidos), leed por el canal estándar el consumo del resto de meses (desde julio a diciembre, ambos incluidos), y retornadlo acumulado al total del consumo de los 6 primeros meses.
- Dados dos números enteros intercambiad los valores de ambas variables siempre que ambas sean diferentes de cero.
- Dado un entero retornad la suma de sus divisores.
5b. Declarad las variables necesarias para llamar a las acciones del ejercicio anterior e indicad cómo se llamarían.
12.10 Soluciones en lenguaje algorítmico
- Solución
function fibonacci(n: integer): integer
var
i, suma, a, b: integer;
end var
if n < 3 then
suma:= 1;
else
a:= 0;
b:= 1;
suma:= 0;
for i:= 1 to n do
suma:= a+b;
b:= a;
a:= suma;
end for
end if
return sum;
end function
algorithm main
var
number: integer;
end var
writeString("Introduce un número entero positivo: ");
number:= readInteger()
writeString("La suma de los ");
writeInteger(number);
writeString(" primeros números de la serie Fibonacci es: ");
writeInteger(fibonacci(n));
end algorithm
- Solución
- Solución
- Solución
4b. Solución
- Solución
5b. Solución