Programación reactiva y patrones funcionales¶

Autor: Miguel Rafael Esteban Martín

Sobre mi¶

Miguel Rafael Esteban Martín (miguel.esteban@logicaalternativa.com)


Blog: LogicaAlternativa.com


Trabajando en: Darwinex


Spoiler¶

Esta charla va en realidad de saber enfrentarse a "tochos" de APIs reactivas como:

  • Mono Reactor

  • Single o Observable RxJava

  • Uni quarkus

  • CompletableFuture

Manifiesto reactivo¶

https://www.reactivemanifesto.org/es¶

1. "El sistema responde a tiempo"¶

2. "El sistema responde aunque haya fallos"¶

3. "El sistema responde bajo variaciones de carga"¶

4. "El sistema está orientado a mensajes"¶

"Es la manera de obtener las demás propiedades"

Que esté orientado a mensajes significa:¶

  • Que el sistema es más modular
  • Preserva el bajo acoplamiento con otros módulos o sistemas
    • Indirección en el tiempo y en el espacio

E implica:¶

  • Comunicación no bloqueante y asíncrona.

En relación al escalado¶

Escalado vertical:¶

  • No requiere ni diseño, ni arquitectura especial vrs Escalado "limitado" y "caro" sin alta disponibilidad.

Escalado horizontal:¶

  • Escalado "infinito" y "barato" y disponer de alta disponibilidad vrs Aplicaciones construidas ex profeso.

Paralelismo y asincronía¶

Paralelismo tiene un coste¶

Adoptar la computación en paralelo tiene un coste en complejidad

Porque todo cambia si tu patron repositorio pasa de

public Person findById( String idPerson )

a

public CompletableFuture<Person> findById( String idPerson )

Hasta ahora...¶

Las APIS reactivas por su mejor gestión del los hilos:

  • Son más eficientes (mismo rendimiento menos recursos)
  • Son más resilentes (más sencillo implementar patrones de estabilidad)
  • Soportan mejor los picos de carga.

Pero por su asíncronia y paralelismo:

  • Son más complejas

Adopción de nuevos patrones¶

In reality all of the patterns in that book can be replaced with basic FP concepts like high-order function, functions composition and pattern matching like I tried to demonstrate years ago with this trivial examples "g ∘ f patterns (aka From Gof to lambda)"

Mario Fusco 🇪🇺🇺🇦 @mariofusco@jvm.social (@mariofusco) November 21, 2022

DSL (Domain Specific Language)¶

El objetivo es crear un Domain Specific Language, utilizando composición monádica. Como ejemplo nos vamos a basar en dos implementaciones una reactiva CompletableFuture y otra que no lo es, basada en Supplier.

  • Transformar el valor de un futuro
  • Unir dos futuros
  • Concatenar el resultado de dos futuros
  • Siendo monádico, significa que será fail fast. La composión permite "olvidarte" del control de errores
  • Permitirá gestionar errores (recuperar desde un error o transformación de errores)

Funtores¶

Definición formal¶

Permite transformar el valor que hay dentro del un "contexto" a través de una función.

Cuando hablamos de contextos hablamos de "tipos de tipos" o "constructores de tipos" como List, Optional, Mono, Observable, etc.

Map
===

[A] --------- { f A -> B } ---------- => [B]

Un ejemplo:¶

 <8420412147> -- { obtenerLibro( isbn: String ): [Libro] } -- => [<El Quijote>]

 ---
 Map
 ===

[<El Quijote>] ---- { numeroCapitulos( libro: Libro ): int } ---- => [<126>]

Definición para CompletableFuture¶

In [2]:
public interface CompletableFutureFuntor {

    <A,B> CompletableFuture<B> map( CompletableFuture<A> future, Function<A,B> f );

}

Definición para Id (basado en Supplier)¶

In [3]:
public interface Id<T> extends Supplier<T>{};

"Evaluación perezosa" de una variable tipo String con el valor Hola mundo

In [4]:
final Id<String> exampleId = () -> "Hola Mundo"
In [5]:
public interface IdFuntor {

    <A,B> Id<B> map( Id<A> id, Function<A,B> f );

}

Aplicativos¶

Un aplicativo también es un funtor. Soluciona la limitación de un sólo argumento y Permite unir dos "contextos" en uno.

Se puede definir de varias maneras, todas ellas son equivalentes.

Definición formal¶

A partir de un contextos con valor A y otro con valor de una función de A -> B obtenemos otro con valor B

Ap
==

[A] ---------- [ { f: A -> B } ] ---------- => [B]

Definición del producto¶

A partir de dos contextos uno con valor A y otro con valor B obtenemos otro con la tupla de los dos valores.

Product
=======

[A] -------------+      
                 |
                 + => [(A,B)]
                 |
[B] -------------+

Definición de Map2¶

A partir de dos contextos de tipo A y B y una función que a partir de los dos tipos se obtiene otro tipo C, se obtiene un contexto de tipo C.

Map2
====

[A] -------------+      
                 |
                 + -- { f: A,B -> C } --  => [C]
                 |
[B] -------------+

Unir el resultado de dos computaciones¶


<8420412147> -- (  obtenerLibro( isbn: String ): [Libro]  ) -- => [<El Quijote>]

<Cervantes>  -- ( obtenerBiografia( autor: Autor): [Biografia] ) -- => [<Bio Cervantes>]
Map2
====

[<El Quijote>] ----+      
                   |
                   + - { crearEnsayo( l: Libro, b: Biografia ): Ensayo } - => [<Vida y obra de Cervantes>]
                   |          
[<Bio Cervantes>] -+

Definición de aplicativo para CompletableFuture¶

In [6]:
public interface CompletableFutureApplicative extends CompletableFutureFuntor  {
    
    <A,B,C> CompletableFuture<C> map2(
        CompletableFuture<A> futureA, 
        CompletableFuture<B> futureB, 
        BiFunction<A,B,C> f 
    );
    
    <A> CompletableFuture<A> pure(A value );

}

Definición de aplicativo para Id¶

In [7]:
public interface IdApplicative extends IdFuntor  {
    
    <A,B,C> Id<C> map2(
        Id<A> idA, 
        Id<B> idB, 
        BiFunction<A,B,C> f 
    );
    
    <A> Id<A> pure(A value );

}

Aplicativo, operaciones derivadas¶

Utilizando el concepto de algebra, a partir de unas operaciones primitivas y unas leyes, se pueden crear nuevas operaciones por composición de estas.

En este caso a partir de las funciones primitivas, map2 y pure, podemos definir:

  • La operación aplicativo
  • La operación producto
  • La operación map del funtor.
In [8]:
public record Tuple<T,U>(T one, U two){};

public interface CompletableFutureApplicative extends CompletableFutureFuntor  {
    
    <A,B,C> CompletableFuture<C> map2(
        CompletableFuture<A> futureA, 
        CompletableFuture<B> futureB, 
        BiFunction<A,B,C> f );
    
    <A> CompletableFuture<A> pure(A value );
    
    /*Other Definitions*/
    
    default <A,B> CompletableFuture<B> ap( 
        CompletableFuture<A> future, 
        CompletableFuture<Function<A,B>> futureFunc ) {
        
        return map2( 
            future, 
            futureFunc, 
            (a, f) -> f.apply(a) 
        );
        
    }
    
    default <A,B> CompletableFuture<Tuple<A,B>> product(
        CompletableFuture<A> futureA, 
        CompletableFuture<B> futureB ) {
        
        return  map2( 
            futureA,
            futureB, 
            (a,b) -> new Tuple<>(a, b) 
         );
        
    }
    
    /*Funtor*/
    
     default <A,B> CompletableFuture<B> map( CompletableFuture<A> future, Function<A,B> f ) {
        return ap( future, pure( f ) );
    }

}
In [9]:
public interface IdApplicative extends IdFuntor {
    
    <A,B,C> Id<C> map2(
        Id<A> idA, 
        Id<B> idB, 
        BiFunction<A,B,C> f );
    
    <A> Id<A> pure(A value );
    
    /*Other Definitions*/
    
    default <A,B> Id<B> ap( 
        Id<A> idA, 
        Id<Function<A,B>> idFunc ) {
        
        return map2( 
            idA, 
            idFunc, 
            (a, f) -> f.apply(a) 
        );
        
    }
    
    default <A,B> Id<Tuple<A,B>> product(
        Id<A> idA, 
        Id<B> idB ) {
        
        return  map2( 
            idA,
            idB, 
            (a,b) -> new Tuple<>(a, b) 
         );
        
    }
    
    /*Funtor*/
    
     default <A,B> Id<B> map( Id<A> idA, Function<A,B> f ) {
        return ap( idA, pure( f ) );
    }

}

Aplicativo Error¶

Permite gestionar errores.

<1111111111> -- { obtenerLibro( isbn: String ): [Libro] } -- => [<Error: ErrorNotFound>]

---

[<Error: ErrorNotFound>] -- { alternativaLibro( error: Error ): [Libro] } => [<La vida es sueño>]

En nuestro caso el tipo de error será Throwable

Definición de aplicativo error para CompletableFuture¶

In [10]:
public interface CompletableFutureApplicativeError extends CompletableFutureApplicative  {
    
    <A> CompletableFuture<A> handleErrorWith(
        CompletableFuture<A>futureA, 
        Function<Throwable,CompletableFuture<A>> f);
    
     <A> CompletableFuture<A> raiseError(Throwable error);

}

Definición de aplicativo error para Id¶

In [11]:
public interface IdApplicativeError extends IdApplicative  {
    
    <A> Id<A> handleErrorWith(
        Id<A>idA, 
        Function<Throwable,Id<A>> f);
    
     <A> Id<A> raiseError(Throwable error);

}

Aplicativo error, operaciones derivadas¶

La operación handleError se puede definir como una especie de "default"

In [12]:
public interface CompletableFutureApplicativeError extends CompletableFutureApplicative  {
    
    
    <A> CompletableFuture<A> handleErrorWith(
        CompletableFuture<A>futureA, 
        Function<Throwable,CompletableFuture<A>> f);
    
     <A> CompletableFuture<A> raiseError(Throwable error);
     
     /*Other*/
     
     default <A> CompletableFuture<A> handleError(
        CompletableFuture<A>futureA, 
        Function<Throwable,A> f) {
         
         return handleErrorWith( futureA, error -> pure( f.apply(error) ) );
        
    }

}
In [13]:
public interface IdApplicativeError extends IdApplicative  {
    
    
    <A> Id<A> handleErrorWith(
        Id<A>idA, 
        Function<Throwable,Id<A>> f);
    
     <A> Id<A> raiseError(Throwable error);
     
     /*Other*/
     
     default <A> Id<A> handleError(
        Id<A>idA, 
        Function<Throwable,A> f) {
         
         return handleErrorWith( idA, error -> pure( f.apply(error) ) );
        
    }

}

MonadError¶

Definición formal de Monada¶

Es un funtor aplicativo monádico. A partir de un contexto de tipo A y una función que tiene como dominio el tipo A y devuelve un contexto de tipo B se obtiene un contexto del tipo B

FlatMap
=======

[A] ---------- { f: A -> [B] } ---------- => [B]

Permite concatenar el resultado de dos computaciones

<8420412147> -- { [Libro] obtenerLibro( isbn: String ) } -- => [<El Quijote>]

---

FlatMap
=======

[<El quijote>]  -- { obtenerReferencias(libro : Libro): [Referencias] } --=> [<Referencias sobre El Quijote>]

Definición de MonadError para CompletableFuture¶

En este caso elegimos MonadError extendiendo de aplicativo error

In [14]:
public interface CompletableFutureMonadError extends CompletableFutureApplicativeError  {
        
    <A,B> CompletableFuture<B> flatMap(
        CompletableFuture<A> futurA, 
        Function<A,CompletableFuture<B>> f );  
   
}

Definición de MonadError para Id¶

In [15]:
public interface IdMonadError extends IdApplicativeError  {
        
    <A,B> Id<B> flatMap(
        Id<A> idA, 
        Function<A,Id<B>> f );  
   
}

Operaciones primitivas y derivadas de MonadError¶

Definiremos las operaciones del aplicativo map2 y la función flatten a partir de las operaciones primitivas de Monada

In [16]:
public interface CompletableFutureMonadError extends CompletableFutureApplicativeError  {
     
    /*Primitives */
    
    <A,B> CompletableFuture<B> flatMap(
        CompletableFuture<A> futureA, 
        Function<A,CompletableFuture<B>> f );  

    <A> CompletableFuture<A> pure(A value );

    <A> CompletableFuture<A> handleErrorWith(
        CompletableFuture<A>futureA, 
        Function<Throwable,CompletableFuture<A>> f);

    <A> CompletableFuture<A> raiseError(Throwable error);
    
     /*Other*/
    
    default <A> CompletableFuture<A> flatten(CompletableFuture<CompletableFuture<A>> future ) {
        return flatMap( future, Function.identity() );
    }
    
    /*Applicative*/
    
    default <A,B,C> CompletableFuture<C> map2(
        CompletableFuture<A> futureA, 
        CompletableFuture<B> futureB, 
        BiFunction<A,B,C> f ) {

         return flatMap(
             futureA,
             a -> flatMap( 
                 futureB, 
                 b ->  pure(f.apply(a, b) )
             )
         );

    }
   
}
In [17]:
public interface IdMonadError extends IdApplicativeError  {
     
    /*Primitives */
    
    <A,B> Id<B> flatMap(
        Id<A> idA, 
        Function<A,Id<B>> f );  

    <A> Id<A> pure(A value );

    <A> Id<A> handleErrorWith(
        Id<A>idA, 
        Function<Throwable,Id<A>> f);

    <A> Id<A> raiseError(Throwable error);
    
     /*Other*/
    
    default <A> Id<A> flatten(Id<Id<A>> idA ) {
        return flatMap( idA, Function.identity() );
    }
    
    /*Applicative*/
    
    default <A,B,C> Id<C> map2(
        Id<A> idA, 
        Id<B> idB, 
        BiFunction<A,B,C> f ) {

         return flatMap(
             idA,
             a -> flatMap( 
                 idB, 
                 b ->  pure(f.apply(a, b) )
             )
         );

    }
   
}

Implementando¶

Por fin....

In [18]:
public record CompletableFutureMonadErrorImpl( Executor executor )  implements CompletableFutureMonadError {

    public <A,B> CompletableFuture<B> flatMap(
        CompletableFuture<A> futureA, 
        Function<A,CompletableFuture<B>> f ){
        
        return futureA.thenComposeAsync(f, executor);
    }
    
    public <A> CompletableFuture<A> pure(A value ){
        return CompletableFuture.completedFuture(value);
    }
    
    public <A> CompletableFuture<A> handleErrorWith(
        CompletableFuture<A>futureA, 
        Function<Throwable,CompletableFuture<A>> f) {
        
        final CompletableFuture<CompletableFuture<A>> res = futureA.handleAsync( 
            (value, error) -> {
                if ( error != null ) {
                    return f.apply( error );
                } else {
                    return pure( value );
                }
            }
        );
        
        return flatten( res );
    }
    
    public <A> CompletableFuture<A> raiseError(Throwable error) {        
        return CompletableFuture.failedFuture(error);
    }

}
In [19]:
class IdMonadErrorImpl implements IdMonadError {
    
    public <A,B> Id<B> flatMap( Id<A> idA, Function<A,Id<B>> f ){
        return () -> f.apply( idA.get() ).get();
    }

    public <A> Id<A> pure(A value ){
        return () -> value;
    }

    public <A> Id<A> handleErrorWith(
        Id<A>idA, 
        Function<Throwable,Id<A>> f) {

        return () -> {
            try{
                return idA.get();
            } catch( RuntimeException e ) {
                return f.apply( e ).get();
            }
        };

    }

    public <A> Id<A> raiseError(Throwable error) {
        return () -> {
            final var res = switch(error) {
                case RuntimeException e ->  e ;
                case default -> new RuntimeException(error) ; 
            };
            throw res;
        };
    }

}

Creando un DSL¶

Domain Specific Language para evitar el infierno de los parentesis ( )

In [20]:
public record CompletableFutureDsl<A> ( CompletableFuture<A> value, CompletableFutureMonadError monad  ){
    
    private final static CompletableFutureMonadError DEFAULT_MONAD = new CompletableFutureMonadErrorImpl( 
        Executors.newCachedThreadPool() 
    );
    
    public static <A>  CompletableFutureDsl<A> from(CompletableFuture<A> value){
        return new CompletableFutureDsl<>( value, DEFAULT_MONAD );
    }

    public <B> CompletableFutureDsl<B> map( Function<A,B> f )  {
        return new CompletableFutureDsl<>( monad.map(value,f), monad );
    }
    
    public <B,C> CompletableFutureDsl<C> zipWith( CompletableFuture<B> other, BiFunction<A,B,C> f )  {
        return new CompletableFutureDsl<>( monad.map2(value, other, f), monad );
    }
    
    public CompletableFutureDsl<A> handleErrorWith( Function<Throwable, CompletableFuture<A>> f )  {
        return new CompletableFutureDsl<>( monad.handleErrorWith(value, f), monad );
    }
    
    public <B> CompletableFutureDsl<B> flatMap( Function<A,CompletableFuture<B>> f )  {    
        return new CompletableFutureDsl<>( monad.flatMap(value,f), monad );
    }

}
In [21]:
public record IdDsl<A> ( Id<A> value, IdMonadError monad  ){
    
    private final static IdMonadError DEFAULT_MONAD = new IdMonadErrorImpl();
    
    public static <A>  IdDsl<A> from(Id<A> value){
        return new IdDsl<>( value, DEFAULT_MONAD );
    }

    public <B> IdDsl<B> map( Function<A,B> f )  {
        return new IdDsl<>( monad.map(value,f), monad );
    }
    
    public <B,C> IdDsl<C> zipWith( Id<B> other, BiFunction<A,B,C> f )  {
        return new IdDsl<>( monad.map2(value, other, f), monad );
    }
    
    public IdDsl<A> handleErrorWith( Function<Throwable, Id<A>> f )  {
        return new IdDsl<>( monad.handleErrorWith(value, f), monad );
    }
    
    public <B> IdDsl<B> flatMap( Function<A,Id<B>> f )  {    
        return new IdDsl<>( monad.flatMap(value,f), monad );
    }

}

Testeando el modelo¶

Conclusiones¶

  • Aunque conllevan un camino de asimilación, los patrones funcionales son útiles en Java.
  • Con las limitaciones de Java, podemos utilizar el mismo DSL para codificar nuestra lógica.
  • Teniendo claro este tipo de conceptos, la rampa de aprendizaje será menos dura al enfrentarnos a un API reactiva

¿Preguntas?¶

¡Gracias!¶