Definición de funciones

Objetivos: Mostrar cómo se puede extender el lenguaje Java con nuevas funciones.

Temas:


Motivación

Escribir un programa que determine la cantidad de combinaciones que se pueden realizar tomando k elementos distintos de un grupo de n elementos. El número de combinaciones está dado por la siguiente fórmula:

    | n |      n!
    |   | = ---------
    | k |   k! (n-k)!
Solución:

Calcular el factorial de un número es simple, pero escribir 3 veces el mismo código para calcular el factorial de distintos números es incómodo. Por otra parte, se triplica la probabilidad de cometer algún error.

Definición de funciones

En clases anteriores vimos que Java posee funciones predefinidas para calcular el seno, la raíz cuadrada, el logaritmo y otras funciones científicas. El problema es que es imposible que un lenguaje de programación suministre todas las funciones que un programador podría necesitar.

Por esta razón, prácticamente todos los lenguajes incorporan algún mecanismo para que los programadores puedan definir sus propias funciones cuando no existe la función predefinida apropiada. Para definir una nueva función, el programador debe escribir el código (programa) que calcula esa función.

Por ejemplo, el siguiente programa calcula el número de combinaciones:

    class Combinaciones extends Program {
      void run() {
        print("Ingrese k ? ");
        int k= readInt();
        print("Ingrese n ? ");
        int n= readInt();
        int combinaciones= fact(n)/(fact(k)*fact(n-k));
        println("Combinaciones= "+combinaciones);
      }
      // Definición de la funcion fact
      int fact(int x) {
        // fact recibe un argumento entero y entrega un resultado entero.
        // El siguiente código calcula el factorial de x
        int prod= 1;
        int i= 1;
        while (i<=x) {
          prod= prod*i;
          i= i+1;
        }
        // Indica qué valor entrega esta función.
        return prod;
      }
    }
(Ver el programa completo en Combinaciones.java.)

El siguiente patrón de programación se usa para definir varias funciones que pueden ser usadas en un programa:

    class ... extends Program {
      void run() {
        ...
      }
      tipo nombre-función ( parámetros ) {
        ... programa que calcula esta función ...
        return expresión;
      }
      ... Más funciones ...
    }
Explicación:


Invocación de una función

La forma general de una invocación de una función es:

    nombre-función ( argumentos )
en donde argumentos son 0, 1, 2 o más expresiones separadas por coma. Por ejemplo:

    fact(n-k)
La semántica de una invocación de función es la siguiente:

Esto se puede apreciar en la siguiente figura que muestra las 3 invocaciones de la función fact a partir de run() cuando n es 7 y k es 2:

En cada invocación el argumento es distinto y por tanto el valor entregado por la función.


Ejercicio 1:

Definir la función repite(s,n) que entrega el string s concatenado con sí mismo n veces. Es decir, se desea que repite("hola",3) entregue (o retorne) como resultado el string "holaholahola".

    class Triangulo extends Program {
      String repite(String s, int n) {
        String r= "";
        int i= 1;
        while (i<=n) {
          r= r+s;
          i= i+1;
        }
        return r;
      }
      void run() {
        int i=1;
        while (i<=8) {
          println(repite("*",i));
          i= i+1;
        }
      }
    }
El programa anterior produce la siguiente salida:

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

Ejercicio 2:

Defina la función esPrimo(n) que entrega verdadero cuando n es un número primo y falso en caso contrario. Utilice cualquier método para determinar si n es primo. Ejemplos:

esPrimo(2) true
esPrimo(3) true
esPrimo(25) false
esPrimo(31) true
esPrimo(9) false

Ejercicio 3:

Escriba un programa que use la función esPrimo para mostrar los números primos entre 200 y 300.


Ejercicio 4:

Defina una función mayor que calcule el máximo valor en un arreglo nativo. Por ejemplo, si el arreglo tab contiene las siguientes asociaciones:

índice valor
0 4.5
1 15.0
2 2.2
3 4.3

entonces:

    println( mayor(tab, 0, 3) ); // despliega 15.0
    println( mayor(tab, 2, 3) ); // despliega 4.3
El primer argumento es el arreglo, el segundo es el índice inicial en el arreglo y el tercero es el índice final.

Solución:

    double mayor(double[] tab, int pri, int ult) {
      int i= pri;
      double max= tab[pri];
      while (i<=ult) {
        if (tab[i]>max)
          max= tab[i];
        i= i+1;
      }
      return max;
    }
Este ejemplo muestra que una función puede recibir como argumentos referencias a objetos. En otros ejemplos veremos que una función puede entregar (retornar) referencias a objetos.


Qué es un procedimiento

Un procedimiento es similar a una función, pero difiere en que no entrega ningún resultado. Su interés radica en las acciones que se realizan durante su invocación. Un ejemplo de procedimiento es println(...). Éste siempre se invoca en forma aislada (no como parte derecha en una asignación):
    println( ... );
Las acciones que realiza la invocación de println consisten en desplegar en pantalla el argumento que recibe. Es ilegal escribir asignaciones como:

    x= println( ... );
porque println no retorna ningún valor.


Definición de procedimiento

Un procedimiento se define y se comporta de la misma forma que una función. La única diferencia es que el tipo retornado por un procedimiento es void. Por ejemplo, el siguiente procedimiento despliega los elementos de un arreglo con valores reales:

    void mostrar(double[] tab, int pri, int ult) {
      int i= pri;
      while (i<=ult) {
        println(i+" -> "+tab[i]);
        i= i+1;
      }
      // no hay return
    }
Ejemplos de uso de mostrar son los siguientes:

    mostrar(tab, 0, 3);
que produce el siguiente resultado en pantalla:

    0 -> 4.5
    1 -> 15.0
    2 -> 2.2
    3 -> 4.3
Ejercicio 5: procedimiento que graba el contenido de un arreglo en un archivo.

Defina el procedimiento grabar que reciba como argumentos un arreglo con valores de tipo double, dos enteros que señalan el rango de llaves válidas (pri y ult) y el nombre de un archivo. Su procedimiento debe producir un archivo con el contenido del arreglo. Por ejemplo, la siguiente invocación:

    grabar(tab, 0, 3, "cont.txt");
debe producir un archivo de nombre "cont.txt" con 4 líneas cuyo contenido es:

    4.5
    15.0
    2.2
    4.3
(Propuesto.)


Procedimientos con efectos laterales

Definir un procedimiento leer que recibe como argumentos un arreglo y el nombre de un archivo con 4 líneas, e inicializa el arreglo con los valores reales contenidos en un archivo. Por ejemplo:

    double[ ] tabB= new double[4];
    leer(tabB, "cont.txt");
    mostrar(tabB, 0, 3);
debe producir el siguiente resultado en pantalla:

    0 -> 4.5
    1 -> 15.0
    2 -> 2.2
    3 -> 4.3
Solución:

    void leer(double[ ] tab, String nom) {
      TextReader lect= new TextReader(nom);
      int i= 0;
      while (true) {
        double x= lect.readDouble();
        if (lect.eofReached())
          break;
        tab[i]= x;
        i= i+1;
      }
      lect.close();
    }
Una función o procedimiento puede modificar los arreglos que recibe como argumentos. En general, pueden producir efectos laterales sobre sus parámetros, cuando son algún tipo de objetos.

En una función o procedimiento, los parámetros que corresponden a algún tipo de objetos son en realidad referencias de objetos (recuerde que una referencia es como el teléfono celular del objeto). Por lo tanto, dentro del procedimiento leer, la variable tab referencia el mismo objeto que es referenciado por la variable tabB en la invocación de leer:


Funciones con efectos laterales

El procedimiento anterior no permite saber cuantos valores se leyeron en el archivo. Por esta razón, es conveniente transformarlo en una función que retorna cuantos elementos habían en el archivo. Por ejemplo:

    double[ ] tabc= new double[1000]; // tabc está vacío
    int n= leer(tabC, "cont.txt");
    mostrar(tabC, 0, n-1);
Para esto hay que cambiar la definición de leer por:

    int leer2(double[ ] tab, String nom) {
      ... // idéntico a leer
      return i; // la última llave inicializada
    }
En este caso leer2 es una función con efectos laterales porque su labor no es sólo retornar un valor, si no que además debe alterar los valores que se encuentran en el arreglo.

Observación:

En la primera versión de leer la asignación:

    int ult= leer(tabB, "cont.txt");
es ilegal porque se definió leer como void. Con la segunda versión sí es válida esta asignación.


Experimento:

¿Qué despliega el siguiente programa?

    int i=1;
    double[ ] tabD= new double[1000];
    int ult= leer2(tabD, "cont.txt");
    println("leidos= "+i);
En este caso, el valor desplegado para i es 1. La variable i que usa la función leer es interna a esa función y no tiene relación alguna con la variable i recién escrita. Diremos que la variable i declarada internamente en la función leer es invisible fuera de la función o procedimiento.


Funciones que retornan arreglos

Una función puede retornar arreglos (u objetos en general). Por ejemplo, se puede construir una función que construye un arreglo y lo entrega a su llamador (el que invoca la función):

    double[ ] tabE= leer3("cont.txt");
    mostrar(tab3);
en donde leer3 es una función que recibe el nombre de un archivo y construye un arreglo con los valores almacenados en el archivo. Se define como:

    double[ ] leer3(String nom) {
      // Se lee una vez el archivo para determinar cuantos reales contiene
      TextReader lect= new TextReader(nom);
      int i= 0;
      while (true) {
        double x= lect.readDouble();
        if (lect.eofReached())
          break;
        i= i+1;
      }
      lect.close();
      int nelem= i;
      // volvemos a leer el archivo para llenarlo
      double[] tab= new double[nelem];
      lect= new TextReader(nom);
      i= 0;
      while (i<nelem) {
        tab[i]= lect.readDouble();
        i= i+1;
      }
      lect.close();
      return tab; // la última llave inicializada
    }
Esta función cuenta los reales contenidos en el archivo, crea un arreglo del tamaño apropiado, coloca los números contenidos en el archivo en el arreglo, y al final retorna la referencia del arreglo. Esta es una función sin efectos laterales, porque no produce ningún cambio en niguno de sus argumentos.


Término anticipado de una función

La línea return que aparece al final de las funciones es una instrucción como cualquier otra y por lo tanto puede aparecer en cualquier parte del código de la función:

    return exp;
Cuando se ejecuta la instrucción return, se termina con la ejecución de la función en donde aparece y la función entrega el resultado de evaluar la expresión que acompaña return. Por ejemplo, la siguiente función calcula si un número es primo:

    boolean esPrimo(int n) {
      int i= 2;
      while (i<n) {
        if (n%i==0) {
          return false;
        }
        i= i+1;
      }
      return true;
    }
La instrucción return en este caso es más ``fuerte'' que un break, puesto que no sólo termina el ciclo, también termina la función completa.