Arquitectura de Software: Algoritmos 4

Dominando Funciones y Procedimientos en Programación

En este artículo veremos funciones (y también procedimientos). Estos componentes son los ladrillos fundamentales para construir software escalable y mantenible.

Divide y vencerás (funciones)

Con esta frase “divide y vencerás”, atribuida a Julio César, los romanos encontraron una estrategia sumamente efectiva para la política y la guerra.

Esta es una idea que también se aplica a la resolución de problemas.

Dado un problema complejo lo dividimos en partes o subproblemas, más simples de resolver.

Diagrama de flujo: Entrada, Proceso y Salida en funciones modulares

Figura 1: Representación de la unidad de procesamiento modular.

Una vez encontrada la solución de cada una de las partes, o subproblemas, las combinamos y resolvemos el problema principal.

Estructura jerárquica de modularización en algoritmos

Llamamos módulo a cada parte, y a todo el proceso, modularización.

Cada módulo resuelve algunos de los subproblemas que conforman el problema original.

La modularización, en programación, consiste en construir un programa a partir de módulos independientes, y todo esto se puede hacer gracias a las funciones.

Nota técnica: Modularidad y Acoplamiento

La modularización no solo simplifica la lógica, sino que permite gestionar el acoplamiento (la dependencia entre módulos) y la cohesión (qué tan enfocada está una función en una sola tarea). Un buen diseño modular reduce la deuda técnica del proyecto.

Funciones

Las funciones son bloques de código que realizan tareas específicas.

Dividen tareas grandes en varias tareas más pequeñas, y permite reutilizar lo que ya hicimos, o incluso lo que otros hicieron, en lugar de comenzar de cero.

Vimos algunas funciones, como leer, imprimir, que son funciones básicas brindadas por los propios lenguajes, con las que podemos armar nuestros algoritmos.

Estructura de las funciones

Una función tiene la forma:

Sintaxis técnica de una función: Tipo de retorno, nombre y argumentos

El nombre de la función, el tipo de dato que retorna y entre paréntesis los parámetros, separados con “,” cada uno con su tipo.

Los parámetros permiten pasar valores a una función para su procesamiento.

Dentro de la función se definen las variables locales, no se ven afuera, y las acciones que ejecuta la función, las instrucciones.

Finalmente retorna el resultado para el invocador.

Momentos de creación y de uso

Hay dos momentos a la hora de pensar en una función:

  • A la hora de crearla, encapsulamos la tarea que ejecuta (fase de declaración/implementación).
  • A la hora de usarla, es como una caja negra, invocamos a la función que necesitamos, nos sirve tomar el resultado, pero no sabemos ni nos interesa cómo llega a ese resultado (fase de llamada o call).

Parámetros

Los parámetros son variables usadas para meter datos adentro de la función, porque la función no conoce lo que está afuera, solo conoce lo que se declara adentro y los parámetros.

Una función puede no recibir parámetros, en este caso la función se define con el paréntesis vacío.

tipo_retorno nombre_funcion()

Las variables que declaramos dentro de la de la función son locales a la función, fuera de la función no existen.

Variables globales

Hay variables que se declaran por fuera (o con alguna clausula como global, según el lenguaje) son variables globales, se pueden usar en cualquier parte del programa.

En programación se recomienda no usar, o usar lo menos posible variables globales para evitar efectos colaterales inesperados (Side Effects).

La práctica es lo que da el criterio de cuándo conviene y cuándo no.

Procedimientos

Hay funciones que no retornan valor, en ese caso el tipo retorno es void.

void nombre_funcion (params)

Son llamados procedimientos, se ocupan de alguna tarea pero no retornan nada. Por ejemplo un procedimiento que guarde datos en disco o imprima en consola.

Estructura de un programa

Los programas suelen estar formados por un programa principal corto (que también es una función, usualmente llamada main) que lo que hace es invocar a las demás funciones.

Ejemplo de aplicación de la modularización

Vamos a pensar cómo podemos aplicar la modularización al siguiente ejercicio:

Enunciado de ejercicio: Geometría modularizada

Este problema lo podemos dividir en módulos: un problema principal dividido en subproblemas más fáciles de resolver.

Por un lado podemos hacer la lectura del radio, por otro lado el cálculo del perímetro, por otro el cálculo del área y por otro lado la visualización de resultados.

Estructura modular: Descomposición de problemas geométricos

De esta forma nos queda el problema dividido en módulos, modularizado.

Ejercicio Funciones: función suma

Planteo de función suma con retorno de datos

Nos pide que usemos una función suma, que va a sumar dos números enteros y retornar el resultado de la suma.

Vamos a tener que pasarle dos parámetros a la función: num1 y num2.

Dentro de la función podemos usar una variable para guardar el resultado de la suma, la llamamos resultado, es el valor de retorno de la función.

Código de la función suma en lenguaje estructurado

Falta armar el programa principal, carga los datos, invoca a la función suma e imprime el resultado.

Código del programa principal invocando módulos externos

El paso a paso

  • El algoritmo inicia con la función principal.
  • La función principal carga los dos números a sumar y llama a la función suma.
  • La función suma realiza su proceso y retorna el resultado a la función que la llamo, en este caso la función principal.
Diagrama de secuencia de ejecución de funciones

Algo importante, esta función suma la podemos reutilizar en cualquier otro programa, no hace falta volver a escribirla.

Ejercicio Funciones: Promedios ponderados

Ejercicio de lógica para cálculo de promedios

Cálculo de promedios ponderados

Van a ingresar 3 notas, con esos 3 datos tenemos que mostrar 3 resultados.

  • promedio ponderado 1
  • promedio ponderado 2
  • promedio simple

Podemos considerar 3 variables para las notas: nota1, nota2, nota3. Pueden ser números con coma, variables de tipo decimal (float o double en lenguajes como C o Java).

Vamos a tener 3 variables para los resultados, también de tipo decimal: promPond1, promPond2, prom.

Busquemos patrones, pensemos cómo se hace en cada caso..

Identificación de patrones matemáticos en promedios

Fórmula para el promedio ponderado

Vamos a aplicar un poco de matemática. ¿Qué pasa cuando multiplicamos un número por 1? ¡Obtenemos el mismo número! Entonces si hacemos:

  • Nota1 * 1 = Nota1
  • Nota2 * 1 = Nota2
  • Nota3 * 1 = Nota3

Podemos decir que el promedio normal es equivalente tener un peso de ponderación 1 en cada nota.

promedio normal = PESO * 1

Podemos escribir el último caso de otra manera:

Promedio = (Nota1 * 1 + Nota2 * 1 + Nota3 * 1) / 3

Ahora, miremos bien, hay cosas parecidas:

Generalización de la fórmula de promedio ponderado

Tenemos nota1 multiplicando un número, el peso de la nota 1 (pesoNota1) sumado a nota2 multiplicado por el peso de la nota 2 (pesoNota2) y eso sumado a nota3 multiplicando por el peso de la nota 3 (pesoNota3).

Esto nos dice que podemos armar una fórmula genérica, en donde cambiando el peso de la nota nos va a dar cada resultado particular.

Con la fórmula genérica ahora podemos crear una función que retorne el resultado de esta fórmula, el promedio ponderado, para las notas y pesos que le pasemos por parámetro.

La podemos llamar funciónPromedioPonderado, va a retornar un número de tipo decimal y como parámetros va a tener las 3 notas y sus respectivos pesos.

Implementación de función promedio genérica

Hacemos el cálculo y lo asignamos a esta variable local resultado. Finalmente retorna el resultado.

Invocación múltiple de la misma función con distintos pesos

Usamos la misma función para los tres casos, solo cambiaron los valores de los parámetros. No tuvimos que escribir tres veces el cálculo, lo escribimos una sola vez. Esto hace que sea menos propenso a errores. si falla el cálculo tenemos que tocar en un solo lugar.

Pasaje de parámetros

Todavía nos queda algo al hablar de funciones, vamos a ver un caso técnico de gestión de memoria.

Análisis de comportamiento de variables en funciones

Tenemos el procedimiento suma, que recibe 3 enteros. Al parámetro num3 le asigna la suma de los otros 2. ¿Qué valor imprime? Antes de responder, hablemos de pasaje de parámetros.

Parámetros por valor y por referencia

Existen dos tipos de pasaje por parámetros:

Ejemplo de un pasaje por valor:

VOID suma (ENTERO num1, ENTERO num2, ENTERO num3)

Ejemplo de un pasaje de referencia:

VOID suma (ENTERO num1, ENTERO num2, REF ENTERO num3)

Esto se aclara en la definición de la función, vamos a considerar que todo los pasajes son por valor a menos que se indique explícitamente que se trata de un pasaje por referencia, con la cláusula REF.

Cuando se pasa por valor

Lo que sucede con la variable dentro del procedimiento no afecta a la variable original, no le cambia el valor. Desde el punto de vista de algoritmos paso el valor que tiene la caja (la variable) y la función trabaja con ese valor.

Desde el punto de vista de la programación, el procedimiento hace una copia de la variable y trabaja con esa copia, por lo que no modifica el valor de la variable original.

Concepto de pasaje por valor: Copia de datos en memoria

Cuando se pasa por referencia

En este caso, lo que pasa en el procedimiento afecta al valor original de la variable, desde el punto de vista de algoritmos paso la caja directamente.

Desde el punto de vista de la programación, vimos que una variable es una posición de memoria en la que se guarda el valor, en ese caso pasa la dirección de memoria para que pueda cambiar su contenido.

Concepto de pasaje por referencia: Acceso directo a memoria
Volvemos al ejercicio

Si la función suma es:

funciones

Se trata de un pasaje por valor así que imprime 10.

En cambio si la función suma es:

funciones

Se trata de un pasaje por referencia de la variable num3, cambia el valor dentro de la función y se ve reflejado afuera, así que imprime 3.

Invocación de funciones

¿Desde una función se puede invocar a otra función?

Claro que sí, incluso podemos invocar funciones de bibliotecas o librerías.

Una biblioteca es un conjunto de funciones agrupadas que resuelven problemas comunes.

Hay bibliotecas de matemática, que agrupan un montón de funciones como potencia, coseno, logaritmo. Alcanza con incluirlas a las librerías de nuestro código para poder utilizarlas, invocando directamente a la función que necesitamos.

Buenas prácticas para las funciones

  • Deben tener pocas líneas
  • Nombre claro de lo que hace cada función
  • Especificación clara de parámetros y resultado
  • Cuanto más genéricas, mejor, ya que se pueden usar en más lugares

Ventajas de usar Funciones

  • Reutilización de código
  • Poder trabajar en paralelo con otros programadores, dividiendo las tareas
  • Vuelve el código más legible
  • Fácil de revisar individualmente cada función

Puede que con los ejemplos con pocas líneas de código que vimos no hayas notado sus beneficios, pero en programas grandes con miles de líneas de código las funciones son fundamentales.


Para consolidar estos conceptos, te propongo los siguientes retos de programación:


  DECIMAL conversor (PALABRA moneda, DECIMAL cantidad)
  INICIO
    DECIMAL tasaConversion    

    SI (moneda == "libra") ENTONCES
      tasaConversion = 1.22
    SINO SI (moneda == "dolar") ENTONCES
      tasaConversion = 0.75
    SINO SI (moneda == "yen") ENTONCES
      tasaConversion = 0.009
    SINO
    INICIO
      IMPRIMIR "Moneda no válida"
      RETORNA -1
    FIN

    RETORNA cantidad * tasaConversion
  FIN
  
  //Programa principal
  INICIO
    PALABRA moneda
    DECIMAL cantidad
    DECIMAL resultado

    IMPRIMIR "Ingrese la moneda (libra, dolar, yen):"
    LEER moneda
    IMPRIMIR "Ingrese la cantidad:"
    LEER cantidad
  
    // Llamamos a la función para realizar la conversión
    resultado = conversor(moneda, cantidad)
  
    // ValidaMOS y mostrar el resultado
    SI (resultado != -1) ENTONCES
      IMPRIMIR "El equivalente en euros es: " + resultado
    FIN  
  FIN
  

  DECIMAL calcularJornal (ENTERO horasTrabajadas, PALABRA turno, PALABRA dia)
  INICIO
    DECIMAL tarifaBase
    DECIMAL incremento
    DECIMAL tarifaFinal
  
    // Determinar la tarifa base según el turno
    SI (turno == "nocturno") ENTONCES
    INICIO
      tarifaBase = 12
      incremento = 0.15  // Incremento en fin de semana
    FIN
    SINO //turno == "diurno"
    INICIO
      tarifaBase = 10
      incremento = 0.10  // Incremento en fin de semana
    FIN
  
    // Ajustar tarifa para fines de semana
    SI (dia == "sabado" O dia == "domingo") ENTONCES
      tarifaFinal = tarifaBase + (tarifaBase * incremento)
    SINO
      tarifaFinal = tarifaBase
    FIN
  
    // Calcular y retornar el jornal diario
    RETORNA horasTrabajadas * tarifaFinal
  FIN
  
  // Programa principal
  INICIO
    ENTERO horasTrabajadas
    PALABRA turno
    PALABRA dia
    DECIMAL resultado
  
    IMPRIMIR "Ingrese el número de horas trabajadas:"
    LEER horasTrabajadas
    IMPRIMIR "Ingrese el turno (diurno, nocturno):"
    LEER turno
    IMPRIMIR "Ingrese el día de la semana:"
    LEER dia
  
    // Llamamos a la función para calcular el jornal
    resultado = calcularJornal(horasTrabajadas, turno, dia)
  
    // Resultado
    IMPRIMIR "El jornal diario es: " + resultado + " €"
  FIN
  

El el siguiente artículo veremos Arrays, una variable que puede almacenar más de un valor, un conjunto de valores.

Recursos utilizados