Con la release de java 8 se abrió un nuevo paradigma en el desarrollo con java, pero es suficiente con lo que trae de serie? Y si pudiéramos tener otras funcionalidades de lenguajes más puramente funcionales en java? Para suplir estas necesidades nació vavr con la misión de reducir el código haciéndolo mas legible y añadir robustez con la inmutabilidad de los datos. Y en este articulo verémos como ser más funcional en Java con Vavr. 

 

Ser más funcional en Java con Vavr

Vavr entre otras cosas incluye inmutabilidad en listas y funciones para trabajar con ellas, incluye algunos de los monads más utilizados en otros lenguajes más funcionales, currying y partial aplication en funciones.

 

Funciones

Composición

Con la llegada de java 8 se incluyó el concepto de Function y BiFunction, con el cual podemos definir funciones de uno o dos parámetros de entrada. por ejemplo:

 
Function<Integer, Integer> pow = (n) -> n * n;
assertThat(pow.apply(2)).isEqualTo(4);
BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
assertThat(multiply.apply(10, 5)).isEqualTo(50);

Con vavr podemos tener funciones hasta 8 parámetros con los tipos FunctionN

 
Function1<Integer, Integer> pow = (n) -> n * n;
assertThat(pow.apply(2)).isEqualTo(4);
Function3<Integer, Integer, Integer, Integer> multiply = (n1, n2, n3) -> n1 * n2 * n3;
assertThat(multiply.apply(5, 4, 3)).isEqualTo(60);

Ademas de poder crear funciones de hasta 8 parámetros de entrada, también nos ofrece la composición de funciones con las operaciones .andThen .apply y .compose

 
Function1<String, String> toUpper = String::toUpperCase;
Function1<String, String> trim = String::trim;
Function1<String, String> cheers = (s) -> String.format("Hello %s", s);
assertThat(trim
            .andThen(toUpper)
            .andThen(cheers)
            .apply("   john")).isEqualTo("Hello JOHN");
Function1<String, String> composedCheer =
cheers.compose(trim).compose(toUpper);
assertThat(composedCheer.apply(" steve ")).isEqualTo("Hello STEVE");


Lifting

Con lifting lo que conseguimos es tratar con las excepciones a la hora de componer las funciones, con lo que la función devolverá un Option.none en el caso de que se haya producido una excepción y un Option.some en el caso de que haya ido todo correctamente.
Esto es muy útil a la hora de componer funciones que usan librerías de terceros que pueden devolver excepciones.

  
Function1<String, String> toUpper = (s) -> {
    if (s.isEmpty()) throw new IllegalArgumentException("input can not be null");
    return s.toUpperCase();
};
Function1<String, String> trim = String::trim;
Function1<String, String> cheers = (s) -> String.format("Hello %s", s);
Function1<String, String> composedCheer = cheers.compose(trim).compose(toUpper);
​
Function1<String, Option> lifted = Function1.lift(composedCheer); 
assertThat(lifted.apply("")).isEqualTo(Option.none());
assertThat(lifted.apply(" steve ")).isEqualTo(Option.some("Hello STEVE"));

 

Aplicación parcial

Con la aplicación parcial podemos crear una nueva función fijándole n parámetros a una ya existente, donde n siempre será inferior a la aridad de la función original y el retorno será una función de aridad original – parámetros fijados. 

 
Function2<String, String, String> cheers = (s1, s2) -> String.format("%s %s", s1, s2);
Function1<String, String> sayHello = cheers.apply("Hello");
Function1<String, String> sayHola = cheers.apply("Hola");
assertThat(sayHola.apply("Juan")).isEqualTo("Hola Juan");
assertThat(sayHello.apply("John")).isEqualTo("Hello John");

Hemos definido una función cheers genérica que acepta dos parámetros de entrada, hemos derivado esta a otras dos nueva sayHello y sayHola aplicándola parcialmente, y ya tenemos dos más especifica para saludar y podríamos derivar en más casos si los necesitáramos.

Currying

Currying es la técnica de descomponer una función de múltiples argumentos en una sucesión de funciones de un argumento.

 
Function3<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
Function1<Integer, Function1<Integer, Integer>> add2 = sum.curried().apply(2);
Function1<Integer, Integer> add2And3 = add2.curried().apply(3);
assertThat(add2And3.apply(4)).isEqualTo(9);

 

Memoization

Una de las premisas de la programación funcional es tener funciones puras, sin side effects, esto básicamente es que a una función pasándole los mismo argumentos siempre ha de devolver el mismo resultado.

Por lo tanto, si siempre devolverá lo mismo, porque no cachearlo? pues esta es la misión de memoization, cachear las entradas y salidas de las funciones para sólo lanzarlas una vez.

 
void memoization() {
        Function1<Integer, Integer> calculate =
            Function1.of(this::aVeryExpensiveMethod).memoized();
        StopWatch watch = new StopWatch();
        watch.start();
        calculate.apply(40);
        System.out.println(watch.getTime());

        calculate.apply(40);
        System.out.println(watch.getTime());

        calculate.apply(50);
        System.out.println(watch.getTime());
}
private Integer aVeryExpensiveMethod(Integer number) {
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return number * number;
}

 

Monads

Try

El monad Try engloba una ejecución capturando una posible excepción, sus dos posibles valores de retorno son el caso de fallo por excepción o el valor resultado si ha ido bien.

Algunos métodos útiles del Try son:


.isSuccess() -> como el propio nombre indica, devuelve un boolean marcando si es un success.
.isFailure() -> devuelve un boolean marcando si es un failure.
get() -> obtiene el valor en el caso de que haya ido correctamente, si se hace un get y no se comprueba de si esto se hace sin comprobar si es success, soltará la excepción.
map() -> map sobre el valor en el caso de que haya ido bien, si es failure no se ejecutará. getOrElse(T) -> el cual permite devolver un valor por defecto en el caso de error. getOrElse(Supplier) -> el cual permite pasarle otra función en el caso de error.
recover( throwable -> {} ) -> Igual que el getOrElse pero en este caso tendremos la excepción que se ha lanzado para poder lograrla o poder devolver diferentes valores dependiendo del tipo de excepción.

 
Function2<Integer, Integer, Integer> divide = (n1, n2) -> n1 / n2;
assertThat(Try.of(() -> divide.apply(10, 0)).isFailure()).isTrue();
assertThat(Try.of(() -> divide.apply(10, 5)).isSuccess()).isTrue();
assertThat(Try.of(() -> divide.apply(10, 5)).get()).isEqualTo(2);
assertThat(Try.of(() -> divide.apply(10, 0)).getOrElse(0)).isEqualTo(0);

 

Lazy

Lazy es un monad sobre un Supplier al cual se le aplica memoization la primera vez que es evaluado.

 
Lazy<List> lazyOperation = Lazy.of(this::getAllActiveUsers);
assertThat(lazyOperation.isEvaluated()).isFalse();
assertThat(lazyOperation.get()).isNotEmpty();
assertThat(lazyOperation.isEvaluated()).isTrue();

 

Either

Either representa un valor de dos tipos, Left y Right siendo por convención, poner el valor en el Right cuando es correcto y en el Left cuando no lo es.

Siempre el resultado será un left o un right, nunca podrá darse el caso de que sean las dos.

 

Estructuras de datos

Listas inmutables

Si uno de los principios de la programación funcional es la inmutabilidad, que pasa cuando definimos una lista y le añadimos ítems? pues que la estamos mutando.

Vavr proporciona una especialización de List a la cual una vez creada ya no puede ser modificada, cualquier operación de añadir, eliminar, reemplazar, nos dará una nueva instancia con los cambios aplicados.

 
import io.vavr.collection.List;
...
//Append
List original = List.of(1,2,3);
List newList = original.append(4);
assertThat(original.size()).isEqualTo(3);
assertThat(newList.size()).isEqualTo(4);
//Remove
List original = List.of(1, 2, 3);
List newList = original.remove(3);
assertThat(original.size()).isEqualTo(3);
assertThat(newList.size()).isEqualTo(2);
//Replace
List original = List.of(1, 2, 4);
List newList = original.replace(4,3);
assertThat(original).contains(1,2,4);
assertThat(newList).contains(1,2,3);

Ademas de la inmutabilidad también proporciona métodos directos para operar con la lista sin pasar por el stream, obtener el valor mínimo, máximo, medio.. para más información sobre lo que ofrece esta lista podéis consultarlo en su javadoc.

Y estas son las principales características que nos ofrece Vavr, pero ofrece muchas más que nos ayudan a poder ser más funcionales en un lenguaje como java.

 

Y no te olvides de suscribirte a nuestro boletín mensual para recibir las últimas noticias sobre Java con Vavr. 

 

Si te gustó este artículo sobre Java con Vavr , te puede gustar:

 

Notas sobre DDD Europe

Arquitectura de microservicios vs arquitectura monolítica 

Scala generics I: clases genéricas y type bounds 

Scala generics II: covarianza y contravarianza 

Principio de responsabilidad única 

Por qué Kotlin?

Patrón MVP en iOS

F-bound en Scala

Sobre Dioses y procrastinación

Arquitectura de microservicios

Fibers en Nodejs

Simular respuestas del servidor con Nodejs

Barcelona como ciudad intelignete

Mapa de los “main players”: ecosistema startup y tech en Barcelona

Ecosistema de salud digital en Barcelona