Futuros simplificados con Scala

Future es una abstracción para representar la finalización de una operación asincrónica. Hoy en día se usa comúnmente en lenguajes populares desde Java hasta Dart. Sin embargo, a medida que las aplicaciones modernas se vuelven más complejas, componerlas también se vuelve más difícil. Scala utiliza un enfoque funcional que facilita la visualización y construcción de la composición futura.

Este artículo tiene como objetivo explicar los conceptos básicos de una manera pragmática. Sin jerga, sin terminología extranjera. Ni siquiera tiene que ser un programador de Scala (todavía). Todo lo que necesita es algo de comprensión de un par de funciones de orden superior: map y foreach. Entonces empecemos.

En Scala, se puede crear un futuro tan simple como esto:

Future {"Hi"} 

Ahora vamos a ejecutarlo y hacer un "Hola Mundo".

Future {"Hi"} .foreach (z => println(z + " World"))

Eso es todo lo que hay. Simplemente ejecutamos un uso futuro foreach, manipulamos un poco el resultado y lo imprimimos en la consola.

Pero, ¿cómo es posible? Así que normalmente asociamos foreach y map con colecciones: desenvolvemos el contenido y jugamos con él. Si lo miras, es conceptualmente similar a un futuro en la forma en que queremos desenvolver la salida Future{}y manipularla. Para que esto suceda, el futuro debe completarse primero, por lo tanto, "ejecutarlo". Este es el razonamiento detrás de la composición funcional de Scala Future.

En aplicaciones realistas, queremos coordinar no solo uno, sino varios futuros a la vez. Un desafío particular es cómo organizarlos para que se ejecuten de forma secuencial o simultánea .

Ejecución secuencial

Cuando varios futuros comienzan uno tras otro como una carrera de relevos, lo llamamos carrera secuencial. Una solución típica sería simplemente colocar una tarea en la devolución de llamada de la tarea anterior, una técnica conocida como encadenamiento. El concepto es correcto pero no se ve bonito.

En Scala, podemos utilizar la comprensión para ayudarnos a abstraerlo. Para ver cómo se ve, vayamos directamente a un ejemplo.

import scala.concurrent.ExecutionContext.Implicits.global object Main extends App { def job(n: Int) = Future { Thread.sleep(1000) println(n) // for demo only as this is side-effecting n + 1 } val f = for { f1 <- job(1) f2 <- job(f1) f3 <- job(f2) f4 <- job(f3) f5  println(s"Done. ${z.size} jobs run")) Thread.sleep(6000) // needed to prevent main thread from quitting // too early }

Lo primero que debe hacer es importar ExecutionContext, cuya función es administrar el grupo de subprocesos. Sin él, nuestro futuro no correrá.

A continuación, definimos nuestro "gran trabajo" que simplemente espera un segundo y devuelve su entrada incrementada en uno.

Luego tenemos nuestro bloque de comprensión. En esta estructura, cada línea interior asigna el resultado de un trabajo a un valor con &lt; - que estará disponible para futuros posteriores. Hemos ordenado nuestros trabajos de modo que, excepto el primero, cada uno tome la salida del trabajo anterior.

Además, tenga en cuenta que el resultado de una comprensión es también un futuro con la producción determinada por el rendimiento. Después de la ejecución, el resultado estará disponible en el interior map. Para nuestro propósito, simplemente colocamos todos los resultados de los trabajos en una lista y tomamos su tamaño.

Vamos a ejecutarlo.

Podemos ver los cinco futuros disparados uno por uno. Es importante tener en cuenta que este arreglo solo debe usarse cuando el futuro depende del futuro anterior.

Ejecución simultánea o paralela

Si los futuros son independientes entre sí, entonces deberían dispararse simultáneamente. Para este propósito, usaremos Future.sequence . El nombre es un poco confuso, pero en principio simplemente toma una lista de futuros y la transforma en una lista de futuros. Sin embargo, la evaluación se realiza de forma asincrónica.

Creemos un ejemplo de futuros mixtos secuenciales y paralelos.

val f = for { f1 <- job(1) f2 <- Future.sequence(List(job(f1), job(f1))) f3 <- job(f2.head) f4 <- Future.sequence(List(job(f3), job(f3))) f5  println(s"Done. $z jobs run in parallel"))

Future.sequence toma una lista de futuros que deseamos ejecutar simultáneamente. Entonces aquí tenemos f2 y f4 que contienen dos trabajos paralelos. Como el argumento introducido en Future.sequence es una lista, el resultado también es una lista. En una aplicación realista, los resultados pueden combinarse para su posterior cálculo. Aquí tomaremos el primer elemento de cada lista y .headluego lo pasaremos a f3 y f5 respectivamente.

Veámoslo en acción:

Podemos ver los trabajos en 2 y 4 disparados simultáneamente indicando un paralelismo exitoso. Vale la pena señalar que la ejecución en paralelo no siempre está garantizada, ya que depende de los subprocesos disponibles. Si no hay suficientes subprocesos, solo algunos de los trabajos se ejecutarán en paralelo. Los demás, sin embargo, esperarán hasta que se liberen algunos hilos más.

Recuperarse de errores

Scala Future incorpora la función de recuperación que actúa como un futuro de respaldo cuando ocurre un error . Esto permite que la composición futura termine incluso con fallas. Para ilustrar, considere este código:

Future {"abc".toInt} .map(z => z + 1)

Por supuesto, esto no funcionará, ya que "abc" no es un int. Con recovery, podemos salvarlo pasando un valor predeterminado. Intentemos pasar un cero:

Future {"abc".toInt} .recover {case e => 0} .map(z => z + 1)

Ahora el código se ejecutará y producirá uno como resultado. En composición, podemos ajustar cada futuro como este para asegurarnos de que el proceso no falle.

Sin embargo, también hay ocasiones en las que queremos rechazar los errores de forma explícita. Para este propósito, podemos usar Future.succesful y Future.failed para señalar el resultado de la validación. Y si no nos importa el fallo individual, podemos posicionar la recuperación para detectar cualquier error dentro de la composición.

Trabajemos otro fragmento de código usando la comprensión que verifique si la entrada es un int válido y menor que 100. Future.failed y Future.successful son ambos futuros, por lo que no es necesario que los envuelva en uno. Future.failed en particular requiere un Throwable, por lo que crearemos uno personalizado para una entrada mayor que 100. Después de ponerlo todo junto, tendríamos lo siguiente:

val input = "5" // let's try "5", "200", and "abc" case class NumberTooLarge() extends Throwable() val f = for { f1 <- Future{ input.toInt } f2  100) { Future.failed(NumberTooLarge()) } else { Future.successful(f1) } } yield f2 f map(println) recover {case e => e.printStackTrace()}

Observe el posicionamiento de recuperar. Con esta configuración, simplemente interceptará cualquier error que ocurra dentro del bloque. Probemos con varias entradas diferentes "5", "200" y "abc":

"5" -> 5 "200" -> NumberTooLarge stacktrace "abc" -> NumberFormatException stacktrace 

“5” llegó al final sin problema. “200” y “abc” llegaron en recuperación. Ahora, ¿qué pasa si queremos manejar cada error por separado? Aquí es donde entra en juego la coincidencia de patrones. Expandiendo el bloque de recuperación, podemos tener algo como esto:

case e => e match { case t: NumberTooLarge => // deal with number > 100 case t: NumberFormatException => // deal with not a number case _ => // deal with any other errors } }

You might probably have guessed it but an all-or-nothing scenario like this is commonly used in public APIs. Such service wouldn’t process invalid input but needs to return a message to inform the client what they did wrong. By separating exceptions, we can pass a custom message for each error. If you like to build such service (with a very fast web framework), head over to my Vert.x article.

The world outside Scala

We have talked a lot about how easy Scala Future is. But is it really? To answer it we need to look at how it’s done in other languages. Arguably the closest language to Scala is Java as both operate on JVM. Furthermore, Java 8 has introduced Concurrency API with CompletableFuture which is also able to chain futures. Let’s rework the first sequence example with it.

That’s sure a lot of stuff. And to code this I had to look up supplyAsync and thenApply among so many methods in the documentation. And even if I know all these methods, they can only be used within the context of the API.

On the other hand, Scala Future is not based on API or external libraries but a functional programming concept that is also used in other aspects of Scala. So with an initial investment in covering the fundamentals, you can reap the reward of less overhead and higher flexibility.

Wrapping up

That’s all for the basics. There’s more to Scala Future but what we have here has covered enough ground to build real-life applications. If you like to read more about Future or Scala, in general, I’d recommend Alvin Alexander tutorials, AllAboutScala, and Sujit Kamthe’s article that offers easy to grasp explanations.