Skip to content

7.6 Programación funcional

1. ¿Qué es la programación funcional?

Paradigma de programación declarativo, no imperativo, se dice cómo es el problema a resolver, en lugar de los pasos a seguir para resolverlo.

La mayoría de lenguajes populares actuales no se pueden considerar funcionales, ni puros ni híbridos, pero han adaptado su sintaxis y funcionalidad para ofrecer parte de este paradigma.

2. Características principales

Transparencia referencial: la salida de una función debe depender sólo de sus argumentos. Si la llamamos varias veces con los mismos argumentos, debe producir siempre el mismo resultado.

Inmutabilidad de los datos: los datos deben ser inmutables para evitar posibles efectos colaterales.

Composición de funciones: las funciones se tratan como datos, de modo que la salida de una función se puede tomar como entrada para la siguiente.

Funciones de primer orden: funciones que permiten tener otras funciones como parámetros, a modo de callbacks.

.

Si llamamos repetidamente a esta función con el parámetro 1, cada vez producirá un resultado distinto (3, 4, 5...).

1
2
3
4
5
6
7
8
class Prueba{
    static int valorExterno = 1;

    static int unaFuncion(int parametro){
        valorExterno++;
        return valorExterno + parametro;
    }
}

Imperativo vs Declarativo

Queremos obtener una sublista con los mayores de edad de entre una lista de personas:

Imperativo:

1
2
3
4
5
List<Persona> adultos = new ArrayList<>();
for (int i = 0; i < personas.size(); i++){
    if (personas.get(i).getEdad() >= 18)
        adultos.add(personas.get(i));
}

Declarativo: List adultos = personas.stream().filter(p -> p.getEdad() >= 18).collect(Collectors.toList()); Se puede observar que el ejemplo declarativo es más compacto, y menos propenso a errores. (Además sirve de ejemplo a la composición de funciones).

3. Funciones Lambda

Son expresiones breves que simplifican la implementación de elementos más costosos en cuanto a líneas de código. También se las conoce como funciones anónimas, no necesitan una clase/nombre. En java se pueden aplicar a la implementación de interfaces, aunque tienen más utilidades prácticas. En algunos lenguajes se les suele denominar "funciones flecha" (arrow functions) ya que en su sintaxis es característica una flecha, que separa la cabecera de la función de su cuerpo.

Comparaciones

API del método List.sort de Java:

default void sort(Comparator<? super E> c)
La interfaz Comparator pide implementar un método compare, que recibe dos datos del tipo a tratar (T), y devuelve un entero indicando si el primero es menor, mayor, o son iguales (de forma similar al método compareTo de la interfaz Comparable.)
int compare (T o1, T o2)
Imaginemos una clase Persona:
1
2
3
4
5
class Persona{
    private String nombre;
    private int edad;
    ...
}
Y un ArrayList personas formada por objetos de tipo Persona:
1
2
3
4
5
6
7
8
...
ArrayList<Persona> personas = new ArrayList<>();
personas.add(new Persona("Nacho", 52));
personas.add(new Persona("David", 47));
personas.add(new Persona("Pepe", 42));
personas.add(new Persona("Maria", 22));
personas.add(new Persona("Marta", 4));
...
Ahora queremos ordenar el ArrayList de personas de mayor a menor edad usando... Implementación "tradicional" java: Comparator oComparable
1
2
3
4
5
6
7
8
...
class ComparadorPersona implements Comparator <Persona>{
    @Override
    public int compare(Persona p1, Persona p2){
        return p2.getEdad() - p1.getEdad();
    }
}
...
1
2
3
4
5
6
...
personas.sort(new ComparadorPersona());
for (int i = 0; i < personas.size(); i++){
 System.out.println(personas.get(i));
}
...     
Sin embargo, implementado con funciones Lambda seria...
1
2
3
4
5
6
...
personas.sort((p1, p2) -> p2.getEdad() - p1.getEdad());
for (int i = 0; i < personas.size(); i++){
    System.out.println(personas.get(i));
}
...

3.1. Estructura de una expresión lambda

(lista de parametros) -> {cuerpo de la función a implementar}

  • El operador lambda (->) separa la declaración de parámetros de la declaración del cuerpo de la función.
  • Los parámetros del lado izquierdo de la flecha se pueden omitir si sólo hay un parámetro. Cuando no se tienen parámetros, o cuando se tienen dos o más, es necesario utilizar paréntesis.
  • El cuerpo de la función son las llaves de la parte derecha se pueden omitir si la única operación a realizar es un simple return.

Funciones Lambda

Las utilizaremos a fondo con las Interfaces.

z -> z + 2 //un sólo parámetro
() -> System.out.println("Mensaje 1") //sin parámetros
(int longitud, int altura) -> { return altura * longitud; } //dos parámetros
1
2
3
4
5
(String x) -> {
 String retorno = x;
 retorno = retorno.concat("***");
 return retorno;
} //un bloque de código más elaborado

4. Gestión de colecciones con streams en Java

Desde Java 8, permiten procesar grandes cantidades de datos aprovechando la paralelización que permita el sistema. No modifican la colección original, sino que crean copias.

Dos tipos de operaciones

  • Intermedias: devuelven otro stream resultado de procesar el anterior de algún modo (filtrado, mapeo), para ir enlazando operaciones
  • Finales: cierran el stream devolviendo algún resultado (colección resultante, cálculo numérico, etc).

Muchas de estas operaciones tienen como parámetro una interfaz, que puede implementarse muy brevemente empleando expresiones lambda

4.1. Filtrado

El método filter es una operación intermedia que permite quedarnos con los datos de una colección que cumplan el criterio indicado como parámetro. filter recibe como parámetro una interfaz Predicate, cuyo método test recibe como parámetro un objeto y devuelve si ese objeto cumple o no una determinada condición.

1
2
3
[...]
Stream<Persona> adultos = personas.stream().filter(p -> p.getEdad() >= 18);
//La función lambda se podría traducir como: "Aquellas personas 'p' de la colección cuya edad sea mayor o igual que 18 años"

4.2. Mapeo

El método map es una operación intermedia que permite transformar la colección original para quedarnos con cierta parte de la información o crear otros datos. map recibe como parámetro una interfaz Function, cuyo método apply recibe como parámetro un objeto y devuelve otro objeto diferente, normalmente derivado del parámetro.

1
2
3
[...]
Stream<Integer> edades = personas.stream().map(p -> p.getEdad());
//La función lambda hace que se añadan al stream de enteros las edades de las personas 'p' de la colección personas.

4.3. Combinar

Se pueden combinar operaciones intermedias (composición de funciones) para producir resultados más complejos. Por ejemplo, las edades de las personas adultas.

1
2
3
4
[...]
Stream<Integer> edadesAdultos = personas.stream()
    .filter(p -> p.getEdad() >= 18).map (p -> p.getEdad());
//Añadiriamos al stream solamente las edades, de aquellas personas que son mayores de edad.

4.4. Ordenar

El método sorted es una operación intermedia que permite ordenar los elementos de una colección según cierto criterio. Por ejemplo, ordenar las personas adultas por edad. sorted recibe como parámetro una interfaz Comparator, que ya conocemos.

1
2
3
4
Stream<Persona> personasOrdenadas = personas.stream()
    .filter(p -> p.getEdad() >= 18)
    .sorted((p1, p2) -> p1.getEdad() - p2.getEdad());
//Para cada pareja de personas p1 y p2, ordénalas en funcion de la resta de la edad de p1 menos la edad de p2 (lo que haciamos en el compareTo)

4.5. Colección

El método collect es una operación final que permite obtener algún tipo de colección a partir de los datos procesados por las operaciones intermedias. Por ejemplo, una lista con las edades de las personas adultas.

List<Integer> edadesAdultos = personas.stream().filter(p -> p.getEdad() >= 18).map(p -> p.getEdad()).collect(Collectors.toList());
//similar a ejemplos anteriores, pero esta vez obtenemos una lista de enteros, en lugar de un stream.

El método collect también permite obtener una cadena de texto que una los elementos resultantes, a través de un separador común. En la función Collectors.joining se puede indicar también un prefijo y un sufijo para el texto.

1
2
3
4
String nombresAdultos = personas.stream().filter(p -> p.getEdad() >= 18)
    .map(p -> p.getNombre())
    .collect(Collectors.joining(", ","Adultos: ",""));
//genera una lista de nombres de personas, con un prefijo, separado y sufijo.

4.6. forEach

El método forEach permite recorrer cada elemento del stream resultante, y hacer lo que se necesite con él. Por ejemplo, sacar por pantalla en líneas separadas los nombres de las personas adultas.

personas.stream().filter(p -> p.getEdad() >= 18)
    .map(p -> p.getNombre()).forEach(p -> System.out.println(p));

4.7. Media aritmética

El método average permite, junto con la operación intermedia mapToInt, obtener una media de un stream que haya producido una colección resultante numérica. Por ejemplo, la media de edades de las personas adultas.

double mediaAdultos = personas.stream().filter(p -> p.getEdad() >= 18)
    .mapToInt(p -> p.getEdad()).average().getAsDouble();