Autor: Miguel Rafael Esteban Martín
Miguel Rafael Esteban Martín (miguel.esteban@logicaalternativa.com)
Blog: LogicaAlternativa.com
Trabajando en: Darwinex
Esta charla va en realidad de saber enfrentarse a "tochos" de APIs reactivas como:
"Es la manera de obtener las demás propiedades"
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 )
Las APIS reactivas por su mejor gestión del los hilos:
Pero por su asíncronia y paralelismo:
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
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
.
<8420412147> -- { obtenerLibro( isbn: String ): [Libro] } -- => [<El Quijote>]
---
Map
===
[<El Quijote>] ---- { numeroCapitulos( libro: Libro ): int } ---- => [<126>]
CompletableFuture
¶public interface CompletableFutureFuntor {
<A,B> CompletableFuture<B> map( CompletableFuture<A> future, Function<A,B> f );
}
Id
(basado en Supplier
)¶public interface Id<T> extends Supplier<T>{};
"Evaluación perezosa" de una variable tipo String
con el valor Hola mundo
final Id<String> exampleId = () -> "Hola Mundo"
public interface IdFuntor {
<A,B> Id<B> map( Id<A> id, Function<A,B> f );
}
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.
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]
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] -------------+
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] -------------+
<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>] -+
CompletableFuture
¶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 );
}
Id
¶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 );
}
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:
map
del funtor.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 ) );
}
}
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 ) );
}
}
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
CompletableFuture
¶public interface CompletableFutureApplicativeError extends CompletableFutureApplicative {
<A> CompletableFuture<A> handleErrorWith(
CompletableFuture<A>futureA,
Function<Throwable,CompletableFuture<A>> f);
<A> CompletableFuture<A> raiseError(Throwable error);
}
Id
¶public interface IdApplicativeError extends IdApplicative {
<A> Id<A> handleErrorWith(
Id<A>idA,
Function<Throwable,Id<A>> f);
<A> Id<A> raiseError(Throwable error);
}
La operación handleError
se puede definir como una especie de "default"
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) ) );
}
}
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) ) );
}
}
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>]
CompletableFuture
¶En este caso elegimos MonadError extendiendo de aplicativo error
public interface CompletableFutureMonadError extends CompletableFutureApplicativeError {
<A,B> CompletableFuture<B> flatMap(
CompletableFuture<A> futurA,
Function<A,CompletableFuture<B>> f );
}
Id
¶public interface IdMonadError extends IdApplicativeError {
<A,B> Id<B> flatMap(
Id<A> idA,
Function<A,Id<B>> f );
}
Definiremos las operaciones del aplicativo map2
y la función flatten
a partir de las operaciones primitivas de Monada
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) )
)
);
}
}
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) )
)
);
}
}
Por fin....
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);
}
}
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;
};
}
}
Domain Specific Language para evitar el infierno de los parentesis (
)
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 );
}
}
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 );
}
}