Introducción a la programación funcional con Java y la librería Vavr

Escrito por el .
java planeta-codigo programacion
Enlace permanente Comentarios

Ciertas propiedades de la programación funcional fueron una de las características más destacadas añadidas a Java 8. La librería Javaslang y más tarde renombrada a Vavr basándose en estas nueva características añade algunas otras que no están incluidas en el propio JDK y están presentes en otros lenguajes más recientes y con programación funcional desde sus inicios. En esta breve introducción a la librería Vavr comentaré cuales son las propiedades que proporciona para simplificar algunas aspectos de la tarea de programación.

Vavr

La programación funcional es un paradigma de programación declarativa que usa múltiples funciones para realizar cálculos y transformaciones a los datos de entrada para producir un resultado, se centra más en las funciones necesarias para realizar los cálculos que en la forma de realizar esos cálculos como ocurre en la programación imperativa. En la programación declarativa el código expresa mejor la intención y suele requerir menos código y estos son algunos motivos de por qué usar la programación funcional.

Las funciones tiene como característica que no cambian los valores de entrada sino que en base a ellos producen unos datos de salida nuevos, otra de sus características es que si se usan los mismos valores de entrada se producen los mismos valores de salida. Un primer paso hacia la programación funcional es usar datos inmutables.

Dicho esto un código no se evalúa únicamente por la cantidad de lineas de código que contiene menos lineas no siempre es mejor si otra variable a tener en cuenta es la legibilidad que facilite la compresión del código a otra persona y a uno mismo en un futuro.

Me parece que ha sido hace mucho menos pero Java 8 fue publicado ya nada más y nada menos que hace casi cuatro años, una de las mayores modificaciones al lenguaje Java desde Java 5. Una de la nuevas características añadidas más destacadas fue la incorporación de las funciones lambda que posibilitan la programación funcional. Las lambdas combinado con los streams, otra de las nuevas características, en las colecciones hacen posible escribir con muchas menos líneas de código y de una forma mucho más natural y legible para la misma funcionalidad que en versiones anteriores. Las nuevas características añadidas al lenguaje en Java 8 ya por si solas son una gran mejora sin embargo a algunos no les parecen suficientes, una de las librerías Java del momento es Vavr que mejora y añade nuevas funcionalidades basándose en las novedades básicas añadidas al lenguaje en la versión 8.

Varv se define a si misma como una librería funcional para Java 8+. Vavr proporciona colecciones inmutables, las necesarias funciones y estructuras de control para operar estos valores. El resultado es bello y simplemente funciona.

Java 8 no incorpora todo lo que podría en el JDK por decisión de los desarrolladores, ya que una de los principios que guían el desarrollo de Java es la compatibilidad hacia atrás e incorporar cosas en el lenguaje o JDK que en un momento determinado hasta que no demuestran que son realmente útiles y beneficiosas. Conservar la compatibilidad hacia atrás es una fortaleza pero que puede dar la sensación que Java siempre va un paso por detrás en incorporar novedades, aún así complementado con librerías como Vavr en el caso de las programación funcional se echa de menos menos otros lenguajes como Kotlin o Clojure.

Jerarquía de clases de Vavr

Jerarquía de clases de Vavr

Los principios que sigue la librería Vavr y que forman la tendencia en la programación y la programación funcional son:

  • Efectos colaterales: los cambios de estado son considerados dañinos si afectan al programa de una forma no deseada.
  • Transparencia referencial: una función o expresión tiene esta propiedad si puede ser reemplazada por su valor sin afectar al comportamiento del programa. Dicho de otra forma, dados los mismos datos de entrada el resultado de la función o expresión es el mismo. Una función es pura si todas las expresiones que contiene tienen la propiedad de transparencia referencial.
  • Pensar en valores: los valores más interesantes son aquellos que son inmutables ya que son thread-safe no necesitando mecanismos de sincronización en su acceso o modificación, son estables para las funciones equals y hashCode siendo confiables como claves hash y no necesitan ser clonados.
  • Estructuras de datos: proporciona una colección rica de estructuras de datos funcionales apoyándose en la funciones lambda. Son un reemplazo para las colecciones estándar de Java, la única interfaz que comparten es Iterable. Las colecciones de Java ocultan los detalles de implementación y las estructuras de datos internas pero son colecciones mutables. Las colecciones inmutables en Java se proporcionan como envoltorios que lanzan una excepción al intentarlas modificar. La colecciones persistentes son efectivamente inmutables realizando pequeñas modificaciones y conservando las estructuras anteriores y nuevas y permitiendo consultar y modificar cualquiera de sus versiones.
  • Estructuras de datos funcionales: son aquellas estructuras de datos inmutables, persistentes y los métodos tienen la propiedad de transparencia referencial.
  • Colecciones: los stream de Java para las colecciones son una forma de recorrerlas, realizar una computación y obtener una nueva colección. Los stream de Vavr no están tan relacionados con las colecciones.

Vavr proporciona una representación bien diseñada de algunos de los tipos más básicos que aparentemente están ausentes o son rudimentarios en Java: Tuple, Value y λ (función lambda). Entre las funcionalidades proporcionadas por Vavr están:

  • Tuplas: son una caja para contener varios objetos evitando la necesidad de crear una clase especifica si esta no tiene ningún comportamiento asociado. Vavr proporciona tuplas hasta de 8 elementos, de Tuple0 a Tuple8.
  • Funciones: la programación funcional se basa en los valores y la transformación de esos valores usando funciones. Java 8 proporciona la clase Function que acepta solo un parámetro y la clase BiFunction que acepta dos. En Vavr las interfaces funcionales van de Function0 hasta Function8. Es posible aplicar conceptos como composition, lifting, partial application, currying o memoization.
  • Valores: los valores en Vavr son inmutables. Algunos objetos Value proporcionados son: Option, Try, Lazy, Either, Future, Validation.
  • Colecciones: se ha diseñado una nueva librería de colecciones para Java que cumple con los requerimientos de la programación funcional e inmutabilidad. List, Queue, SortedSet, Seq, Set, Map, Stream, Traversable, …

Jerarquía de clases de Seq Jerarquía de clases de Set y Map

Jerarquía de clases de Seq, Set y Map
  • Pattern matching: son una especie de sentencia switch que son evaluadas como una expresión y que retornan un valor.

Entre la documentación de Varv hay numerosos ejemplos de código de todo lo anterior que recomiendo leer. En el siguiente ejemplo de código incluyo algunos posibles casos de uso de varias de las funcionalidades anteriores. En definitiva la librería Vavr complementa y añade a Java algunas de las características presentes en la programación funcional de otros lenguajes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package io.github.picodotdev.blogbitix.vavr;

import io.vavr.*;
import io.vavr.Lazy;
import io.vavr.collection.List;
import io.vavr.collection.Stream;
import io.vavr.control.Either;
import io.vavr.control.Option;
import io.vavr.control.Try;

import java.math.BigDecimal;

import static io.vavr.API.*;
import static io.vavr.Predicates.instanceOf;

public class Main {
    public static void main(String[] args) throws Throwable {
        System.out.println("# Side-Effects");
        {
            CheckedFunction2<Integer, Integer, Integer> divide = (Integer dividend, Integer divisor) -> dividend / divisor;
            Try.of(() -> divide.apply(4, 2)).onSuccess(System.out::println).onFailure(e -> e.printStackTrace());
            Try.of(() -> divide.apply(4, 0)).onSuccess(System.out::println).onFailure(e -> e.printStackTrace());
        }

        System.out.println("");
        System.out.println("# Functional Data Structures");
        {
            List.of(0, 2, 3).tail().prepend(1).map(o -> o * 2).forEach(System.out::println);
        }

        System.out.println("");
        System.out.println("# Collections");
        {
            Stream<Integer> list = List.of(0, 2, 3).tail().prepend(1).toStream();
            for (Integer i : list) {
                System.out.println(i);
            }
        }

        System.out.println("");
        System.out.println("# Tuples");
        {
            Tuple3<String, Integer, BigDecimal> tuple = new Tuple3<>("El señor de los anillos", 1272, new BigDecimal("10.40"));
            System.out.printf("Titulo: %s, Páginas: %d, Precio: %.2f%n", tuple._1, tuple._2, tuple._3);
        }

        System.out.println("");
        System.out.println("# Values");
        {
            Option.of("foo");

            Lazy<Double> lazy = Lazy.of(Math::random);
            System.out.println(lazy.get());
            System.out.println(lazy.get());

            CheckedFunction2<Integer, Integer, Integer> divide = (Integer dividend, Integer divisor) -> dividend / divisor;
            Either<Throwable, Integer> value1 = Try.of(() -> divide.apply(4, 2)).toEither();
            Either<Throwable, Integer> value2 = Try.of(() -> divide.apply(4, 0)).toEither();

            value1.right().peek(System.out::println);
            value2.left().peek(e -> e.printStackTrace());

            CheckedFunction1<Integer, Integer> curriedDivide = divide.curried().apply(10);
            System.out.println(curriedDivide.apply(2));

            Function0<Double> hashCache = Function0.of(Math::random).memoized();

            System.out.println(hashCache.apply());
            System.out.println(hashCache.apply());

            Number number = new Double(4.34);
            Option<Number> plusOne = Match(number).option(
                Case($(instanceOf(Integer.class)), i -> i + 1),
                Case($(instanceOf(Double.class)), d -> d + 2)
            );
            System.out.println(plusOne.get());
        }
    }
}
Main.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Side-Effects
2
java.lang.ArithmeticException: / by zero
	at io.github.picodotdev.blogbitix.vavr.Main.lambda$main$8fab2fe6$1(Main.java:20)

	at io.github.picodotdev.blogbitix.vavr.Main.lambda$main$a54cc6af$2(Main.java:22)
# Functional Data Structures
	at io.vavr.control.Try.of(Try.java:75)
	at io.github.picodotdev.blogbitix.vavr.Main.main(Main.java:22)
2
4
6

# Collections
1
2
3

# Tuples
Titulo: El señor de los anillos, Páginas: 1272, Precio: 10,40

# Values
0.9017284202792532
0.9017284202792532
2
java.lang.ArithmeticException: / by zero
	at io.github.picodotdev.blogbitix.vavr.Main.lambda$main$8fab2fe6$2(Main.java:56)
	at io.github.picodotdev.blogbitix.vavr.Main.lambda$main$d82d4ab3$1(Main.java:58)
	at io.vavr.control.Try.of(Try.java:75)
	at io.github.picodotdev.blogbitix.vavr.Main.main(Main.java:58)
5
0.5015851774222647
0.5015851774222647
6.34
System.out
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apply plugin: 'java'
apply plugin: 'application'

repositories {
    jcenter()
}

dependencies {
    compile "io.vavr:vavr:0.9.2"
}

mainClassName = 'io.github.picodotdev.blogbitix.vavr.Main'
build.gradle

La librería Varv es una de las que pongo como ejemplo en el artículo La triste realidad de Java, versiones antiguas, si es útil no usarla puede hacer que programar en Java no sea todo lo placentero que podría ser. Algunas de las críticas que recibe Java no tienen validez usando las herramientas o librerías adecuadas porque en cierta medida las herramientas importan aunque por ahí circule la idea de que las herramientas no importan, para una persona de negocio quizá no sin embargo para un desarrollador sí.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: