Futuros y promesas,… y también monadas. Implementando el patrón

«Los modelos NIO no bloqueantes y la programación asíncrona y reactiva son paradigmas que poco a poco van adoptándose y utilizando más

Los modelos NIO no bloqueantes y la programación asíncrona y reactiva son paradigmas que poco a poco van adoptándose y utilizando más. No creo que diga una tontería al afirmar que muy probablemente serán la norma en un futuro cercano. La razón es clara: aunque no se obtiene un rendimiento mejor que los modelos tradicionales sincrónicos, si son modelos mucho más eficientes. Obtienen el mismo rendimiento con menos recursos al tener un mejor control de los hilos, uno de los recursos más preciados en un sistema operativo.

Futuros y promesas

Futuros y promesas

Para poder articular una programación asíncrona y reactiva es necesario utilizar patrones adaptados a este tipo de programación. Puedes utilizar el patrón observable o las señales,  pero casi podríamos decir que tu camino te llevará irremisiblemente al patrón futuro en cuando tienes que integrar varios resultados asíncronos. Si además nuestra implementación del patrón futuros-promesas cumple con las propiedades de las monadas se nos abre un mudo de posibilidades. Con la programación funcional, la composición de futuros se podrá hacer de una manera elegante obteniendo un código más legible.

Esto permite realizar DSL’s (Domain Specific Language) y separar la lógica de negocio de los efectos de lado. Por ahora “… ahí lo dejo”; prefiero no perder foco en el objetivo de este artículo, pero en mi mente esta tratar todo esto en una nueva reseña. En este artículo entraré a desarrollar todos estos nuevos conceptos intentando desmitificarlos, porque al principio todo lo de “Futuros”, “Promesas” y “Mónadas” puede que suene “muy místico”. Lo haré apoyado en mi propia implementación del patrón Futuros y promesas que está disponible en mi GitHub.

[Logo GitHub] AlternativeFuture

La implementación está basada “fuertemente” en el Api de Scala de futuros y sólo recordar que Akka en su versión de Java la exporta directamente y que además los futuros de Scala son también monadas. Cuando digo que me apoyo en el Api de Scala me refiero a seguir la definición de su contrato, de hecho, no me interesaba el código fuente y si hacer el ejercicio de, a partir de su interfaz, realizar mi propia implementación en Java. El código que podéis ver y utilizar es todo mio. Hecha esta aclaración es el momento de empezar a entrar en faena.

Intentaré hacer un acercamiento de dentro a fuera. Primero pondré el foco en las ideas principales para ir después abriendo el zoom y conseguir tener la imagen completa.

Futuros

«¿Qué es un futuro?» Pongámonos en contexto. Imaginemos dos procesos: uno será el principal que llamará a un método que devuelve un futuro de un valor. El otro será el que calcule realmente el valor de manera asíncrona ejecutado en un segundo plano con respecto al principal. Voy a poner un ejemplo para hacerlo más didáctico. Ana (el hilo A, el principal) le dice a Bartolo (el hilo B) que haga la colada (llamada al método)

Llamada asíncrona

1. Llamada asíncrona

El proceso principal obtiene un futuro como resultado de llamar a un método que se lanzará en segundo plano gracias al hilo B. En el ejemplo Bartolo manda una nota (representa el futuro) a Ana que le permitirá apuntar lo  que se quiere que se haga con la colada cuando haya terminado Bartolo. El hilo A no queda a la espera del resultado de la acción ejecutada por el hilo B y queda liberado para realizar otras operaciones. En el ejemplo Ana no espera a que Bartolo termine y  puede seguir haciendo otras tareas.

2. Se devuelve un futuro

2. Se devuelve un futuro

Este futuro es en realidad un artefacto que permite asignar, desde el proceso principal una función callback. En el siguiente paso, Ana decidirá que es lo que quiere hacer cuando termine la colada, es decir, asignará la función callback que se ejecutará cuando se obtenga el resultado del segundo proceso ejecutado por Bartolo.

Asignar callback

3. Asignar callback

Cuando, gracias al segundo proceso, se haya calculado finalmente el valor, se ejecutará la función de regreso al que se le pasará como argumento el valor final obtenido. En el ejemplo tenemos la función «planchar la ropa» y el resultado del proceso: «la ropa lavada».

Resultado del futuro

4. Resultado del futuro

Contextos de ejecución

La ejecución de las funciones callback con el valor final como argumento se hará dentro de lo que se llama un contexto de ejecución. Este no es más que un pool de hilos que permite independizar la ejecución de la función con el valor final tanto del hilo principal como del segundo proceso, es decir, se ejecutarán en un tercer hilo “reutilizable”. En el ejemplo que estamos siguiendo es Carlos (el hilo C) el que plancha la  ropa.

Ejecución de la función callback con el valor del futuro

Ejecución de la función callback con el valor del futuro

En la implementación del patrón que he propuesto a cada una de las funciones callback y de orden superior definidas en el interfaz futuro se le puede pasar un contexto de ejecución.

Más cosas de los futuros

A grandes rasgos, esta es la esencia: el futuro permite configurar desde la ejecución principal que hacer cuando finalmente exista el valor. Y se hace sin tener que bloquear hilos porque sino arruinamos la asincronía.

Teniendo esto en mente y sin perder de vista lo simple de este concepto, base de todo, voy a ir añadiendo nuevos conceptos e ideas que nos permitirá ir completando la visión general del patrón.

Dos caminos

Añadiendo una nueva capa a las funciones callback. El resultado de la ejecución de cualquier método puede ser, o bien su valor, o bien que su ejecución se haya saldado con un error, luego el resultado final que contiene un futuro puede ser:

  • un valor correcto
  • o también una excepción.

¿Por qué no contemplar esta circunstancia con dos caminos? Un camino para cuando el valor sea correcto con su correspondiente función callback (el método onSuccess) y otro que nos permita definir otra función de regreso cuando se produzca un error (el método onFailure)


Signatura de un onSuccess y onFailure de AlternativeFuture[T]:

onSuccess

void onSuccesful( final Consumer<T> function, final Executor executor );

El argumento function es una función cuyo argumento es de tipo T (es el tipo del valor final del futuro) y resultado void.

function: T => void

Esta será la función que se ejecutaría cuando de resultado final del futuro ha ido bien.

onFailure

void onFailure ( final Consumer<Throwable> function, final Executor executor );

En este caso el argumento function es una función cuyo argumento es un Throwable (la excepción que se reportaría si se produciría un error) y resultado void.

function: Throwable => void

Está será la función que se ejecutará cuando el futuro se haya solventado con un error.


¿Cuando se ejecuta la función callback?

Para dar respuesta a esta pregunta nos debemos fijar en que existan las dos cosas necesarias para la ejecución de la función: que exista la función y que exista el valor. Hay entonces dos posibilidades:

  • Puede que ya haya asignada una o varias funciones callback al futuro, entonces la ejecución de las mismas será cuando se obtenga el valor.
  • O bien puede ser que se obtenga primero el valor y el disparador de que la función se ejecute será la propia asignación de la función callback.

Es importante tener en mente que un futuro (“barra” promesa, esto lo explicaré más adelante), sólo se le puede asignar el valor una vez, además otra característica es que una función callback sólo se ejecutará una vez. Sin embargo se puede asignar al futuro, a lo largo del tiempo, todas las funciones callback que se deseen.

Promesas

Si has llegado hasta aquí probablemente ya te habrá asaltado la duda: ¿Cómo se da valor al futuro? ¿Cómo se le asigna un valor? La respuesta: a través de la promesa.

Podemos decir que la promesa y el futuro son las dos caras de la misma moneda y que una promesa está relacionada con un futuro. La idea básica que debemos tener en mente es la siguiente:

  • el futuro permite asignar la función callback para tomar decisiones una vez se haya obtenido el valor
  • la promesa permite dar valor a ese futuro.

Si volvemos al ejemplo anterior, será más sencillo de entender. Si recordamos el proceso principal llamaba a un método que permite en un segundo proceso calcular un valor de manera asíncrona en un tiempo indeterminado (puede ser antes o después). Mientras que el proceso principal tiene como herramienta al interfaz futuro que le permite indicar que es lo que se debe de hacer una vez se haya calculado el valor, el interfaz de la promesa será el instrumento que poseé  el segundo proceso encargado de calcular el valor para asignar el valor a ese futuro. En síntesis Ana usaría el futuro y Bartolo la promesa.

Diagrama de flujo Promesa-Futuro

Diagrama de flujo Promesa-Futuro

Diagrama de secuencia Promesa-Futuro

Diagrama de secuencia Promesa-Futuro


Implementación

Si echáis un vistazo al código, podéis observar que sólo existe una implementación que implementa las dos interfaces: la promesa y el futuro.

La clase AlternativePromiseImp tiene un atributo que permite almacenar el valor del futuro. Este se asignará gracias a los métodos que expone el interfaz promesa AlternativePromise.

Existen dos atributos más en AlternativePromiseImp que son del tipo de cola FIFO (el primero que entra será el primero que sale). Permiten almacenar las funciones callback gracias a los métodos expuestos por el interfaz AlternativeFuture.

Una de las colas guardará las funciones que se van a ejecutar cuando mediante la promesa relacionada se asigne un valor correcto al futuro. Las funciones se van añadiendo a esta cola utilizando el método ‘onSuccesful‘ del interfaz AlternativeFuture.

La otra cola FIFO almacenará las funciones callback que se ejecutarán cuando la promesa relacionada se resuelva con un error. De la misma manera, se van añadiendo estas las funciones gracias al método ‘onFailure‘ del interfaz AlternativeFuture.

Bien cuando se asigna un valor a la promesa o bien cuando se añade una función callback al futuro se comprueba que existan los requisitos necesarios (la función y el valor del futuro). Si es así se ejecutará las funciones callback incluidas en la cola de callbacks con el valor final. Según se vayan ejecutando las funciones se eliminarán de la cola.


El Futuro como mónada. Programación funcional con AlternativeFuture

Se puede dar una vuelta de tuerca más y seguir añadiendo capas a la cebolla. El patrón Futuro/promesa permite adoptar un nuevo patrón funcional: la monada. Este artículo ya es lo suficientemente extenso, quizás con ya demasiados conceptos así que dejo pospuesto introducir la composición mónadica con AlternativeFuture y os emplazo a un siguiente post continuación de este.

M.E.