Funciones

Funciones (Java)

Las funciones son bloques de código reutilizables que creamos para hacer más fácil el proceso de mantenimiento e implementación de dicho bloque de código. Las funciones tienen varias partes:

  • La firma
  • El cuerpo
  • La salida o retorno

La firma de una función por lo general establece varios conceptos dependiendo del lenguaje de programación en el que se escriba esta. Por ejemplo, en Java una función va a estar compuesta por el ámbito o alcance de la función, el tipo de dato de retorno, el nombre de la función y los parámetros de entrada (opcionales)

public double calculateCylinderVolume(double radius, double height) {
    return Math.PI * radius * radius * height;
}

En este ejemplo el alcance de la función es público. El tipo de retorno es double (es decir que devuelve un tipo de dato double). El nombre de la función que es calculateCylinderVolume y finalmente tenemos los parámetros de entrada que son dos parámetros ambos de tipo double: radius y height respectivamente.

El cuerpo de la función en este caso esta definido por aquellas líneas de código que se encuentran contenidas entre las llaves: {}

Finalmente tenemos el retorno de la función definido por la palabra reservada return, seguido del calculo para determinar la capacidad o volumen de dicho cilindro.

Esta es la declaración de la función, es decir cuando plasmamos que es lo que la función debe hacer y como debe hacerlo. Luego tenemos la invocación de la función, que es cuando de hecho la usamos en otro fragmento de código:

public void example() {
    Scanner scanner = new Scanner(System.in);

    System.out.print("Ingresa el radio del cilindro: ");
    double radius = scanner.nextDouble();

    System.out.print("Ingresa la altura del cilindro: ");
    double height = scanner.nextDouble();

    double volume = calculateCylinderVolume(radius, height);

    System.out.printf("La capacidad cúbica (volumen) del cilindro es: %.2f unidades cúbicas.%n", volume);

    scanner.close();
}

En este ejemplo vemos como se llama a la función asignando a una variable llamada volume lo que retorna la función calculateCylinderVolume. Aquí la función es invocada, y para dicha invocación se deben enviar a la función como argumentos los valores del radius y del height que se obtienen en las lineas de código superiores.

Parámetros y argumentos

Durante la invocación de una función, los valores de entrada se conocen como argumentos. Durante la declaración de la función los valores de entrada se conocen como parámetros.

Los parámetros de una función pueden ser de cualquier tipo de dato nativo o definido por el usuario. Los parámetros pueden usarse por valor o por referencia.

Parámetros por valor o por referencia

Los parámetros pasados por valor crean una copia que puede ser alterada dentro de la función sin cambiar el valor de dicha variable fuera de la función. Es decir, cuando la función es invocada, si radius se pasa por valor y es alterada dentro de la función calculateCylinderVolume, el valor de radius seguirá siendo el mismo en el contexto donde la función es invocada. Es decir, el argumento sigue siendo el mismo antes y después de llamar a la función.

En el caso de los parámetros por referencia, cuando se cambia el valor de la variable dentro del contexto de la función, este cambio se mantiene fuera del contexto de la función, es decir, vamos a ver alterado el valor de radius luego de que se invoque la función calculateCylinderVolume si dentro de esta función el valor de radius cambia. Esto se da porque por referencia no se pasa una copia del argumento enviado a la función, sino que se pasa la referencia de la posición en la memoria dónde se encuentra almacenada dicha variable.

En Java todos los datos primitivos pasan siempre por valor, pero los objetos pasan por referencia, lo que quiere decir que siempre que una función espere recibir un parámetro de un tipo de dato definido por el usuario, este objeto va a modificar su valor (paso por referencia) si el valor de los atributos del objeto son alterados dentro de la función. En otras palabras, todo argumento primitivo pasa por valor pero los objetos pasan como argumentos por referencia a dicha función.

Principios fundamentales de las funciones

Las funciones en programación deben seguir ciertos principios fundamentales para su correcta definición e implementación:

  1. Abstracción: Abstraer un funcionamiento a un nombre atómico
  2. Modularidad: Cada función es un módulo con una tarea específica. Aplica el principio de Responsabilidad Única o SRP (por sus siglas en inglés de Single Responsibility Principle).
  3. Reutilización: Permiten reutilizar el código dentro de la función sin tener que copiarlo o volverlo a escribir, solo llamándola por su nombre.
  4. Encapsulamiento: El código de la función no necesita conocerse para poder utilizarse, lo que simplifica su uso.

Tipos de funciones

Funciones puras

Las funciones puras son aquellas que siempre devuelven el mismo valor ante las mismas entradas y bueno modifican el estado de ningún elemento fuera de su ámbito de ejecución. Cumplen entonces dos criterios:

  1. Mismo resultado ante los mismos parámetros de entrada
  2. No tienen efectos secundarios

Las funciones puras tienen las siguientes características:

  • Son deterministas
  • No tienen dependencias externas
  • No tienen efectos secundarios
  • Se pueden probar y predecir su resultado facilmente

Entonces, nuestra función calculateCylinderVolume no es una función pura ya que depende del valor que devuelva la función Math.PI para funcionar y este valor al depender de la librería Math que es una dependencia externa, impide que se cumplan todas las características de una función pura. Un ejemplo de función pura sería por ejemplo esta para calcular el área de un cuadrado:

public static int square(int number) {
    return number * number;
}

Funciones impuras

Son aquellas funciones que no son necesariamente deterministas, que tienen dependencias externas y que pueden o no alterar otros elementos fuera del ámbito de la función, vamos que una función impura es toda aquella función que no es pura.

Algunos ejemplos de funciones impuras son:

public int deleteRecordsById(SpannerTransaction transaction, String id) {
    String sql = "DELETE FROM Records WHERE Id = @id";

    long deletedRows = transaction.executeUpdate(
        Statement.newBuilder(sql)
            .bind("id").to(id)
            .build()
        );

    return (int) deletedRows;
}

En este ejemplo, la cantidad de registros que se retornan como eliminados va a depender directamente de la cantidad de registros que se encuentren asociados a la tabla Records para el id recibido. Esto hace que no siempre podamos determinar cual vaya a ser el resultado retornado por la función con la misma facilidad todo el tiempo. Además de no ser deteriminista, esta función tiene el efecto secundario de alterar el estado de la tabla Records eliminando registros en la fuente de datos.

Funciones recursivas

Son funciones que se llaman a sí mismas hasta encontrar un valor dado de retorno.

public class Fibonacci {

    public static int fibonacci(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        return fibonacci(n - 1) + fibonacci(n - 2);
    }

    public static void main(String[] args) {
        int numero = 10;
        System.out.println("Fibonacci de " + numero + " es: " + fibonacci(numero));
    }
}

En este caso tenemos una función pura recursiva que permite encontrar el fibonacci de un numero específico en este caso 10. Podemos tener funciones recursivas impuras también como en el caso de esta función que permite encontrar cierto elemento en un grafo:

import java.util.List;
import java.util.stream.Collectors;

public class MapFilterExample {
    public static void main(String[] args) {
        List<String> words = List.of("Lambda", "stream", "Java", "functional", "map", "Monad");
        List<Integer> lengths = words.stream()
                                     .filter(s -> s.toLowerCase().contains("m"))
                                     .map(s -> s.length())
                                     .collect(Collectors.toList());
        System.out.println(lengths);
    }
}

Esta función recursiva tiene algunos efectos secundarios como la impresión con System.out.println además de alterar el valor del contador a nivel de la clase.

Funciones anónimas

Son funciones que no tienen nombre. Estas funciones se definen como funciones Lambda y en Java existen desde Java 8 en adelante. Estas funciones se utilizan en contextos atómicos y específicos para realizar procedimientos rápidos. Un ejemplo de esto es su uso en streams:

import java.util.List;
import java.util.stream.Collectors;

public class MapFilterExample {
    public static void main(String[] args) {
        List<String> words = List.of("Lambda", "stream", "Java", "functional", "map", "Monad");
        List<Integer> lengths = words.stream()
                                     .filter(s -> s.toLowerCase().contains("m"))
                                     .map(s -> s.length())
                                     .collect(Collectors.toList());
        System.out.println(lengths);
    }
}

Las funciones lambda tienen por características el uso de la flecha ->

En este ejemplo tenemos las lambdas: s -> s.toLowerCase().contains(“m”) que permite filtrar la lista de elementos a solo aquellos que contienen la letra m y luego el mapeo s -> s.length() que permite devolver el lago de las palabras que han sido seleccionadas.

También podemos crear funciones anónimas o lambdas para asignar la función a una variable fuera de un contexto específico como en el caso de los streams pero usando la interfaz Function:

Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5);
//Imprimiria 25

La interfaz Function tiene dos parámetros, el primero es el valor de entrada, el segundo es el valor de salida, no necesita ser necesariamente un valor de tipo interno, puede ser un tipo String también tal que:

Function<String, Integer> getLength = x -> x.length();
System.out.println(getLength.apply("lambda"));
//Imprimiria 6

Las funciones anónimas son extremadamente importantes para poder operar streams y también para trabajar con funciones de orden superior.

Funciones de orden superior en Java

Son funciones que reciben como parámetro de entrada a otra función y que puede también devolver una función como parámetro de retorno.

import java.util.function.Function;
import java.util.List;
import java.util.stream.Collectors;

public class Transformer {
    public static <T,R> List<R> transform(List<T> list, Function<T,R> fn) {
        return list.stream().map(fn).collect(Collectors.toList());
    }

    public static void main(String[] args) {
        List<String> names = List.of("Ada", "Grace", "James");
        List<Integer> lengths = transform(names, String::length);
        System.out.println(lengths);   // [3, 5, 5]
    }
}

Esta función busca aplicar una transformación a una colección usando streams, en lugar de asignar una lambda especifica en el map, se recibe como parámetro la función que se desea aplicar, esta función se envía como argumento al stream en el map y luego genera una colección de otro tipo distinto generado por la transformación definida por la función fn. En este ejemplo específico recibe nombres y devuelve las longitudes de cada palabra, y la función enviada es String::length que es justamente la invocación de la función length() sobre los strings usando el azúcar sintáctica conocida como referencia de método que se aplica sobre expresiones lambda equivalentes, es lo mismo que haber enviado s -> s.length()

El alcance de las funciones en Java

El alcance de una función determina su visibilidad, es decir determina desde dónde puede llamarse a la función

  • private: la función puede ser invocada exclusivamente dentro de la clase que la define
private double divide(double a, double b) {
    return a / b;
}
  • public: la función puede ser invocada desde dentro y desde fuera de la clase.
public int add(int a, int b) {
    return a + b;
}
  • protected: la función puede ser invocada dentro de la clase y dentro de cualquier clase heredera de la clase.
protected int subtract(int a, int b) {
    return a - b;
}
  • sin palabra reservada para el modificador de acceso: la función puede ser invocada desde cualquier parte del mismo paquete
int multiply(int a, int b) {
    return a * b;
}

Espero que este post te permita comprender mejor lo que son las funciones en programación y su implementación especifica en Java. Revisa tu proyecto y clasifica tus funciones según sus efectos secundarios, determina las partes de sus firmas y busca funciones de orden superior en tus proyectos o los proyectos de otros. Recuerda que a programar solo se aprende programando.