Translate

sábado, 28 de enero de 2023

Foldable y Traverse parte 5

foldLeft y foldRight son métodos de iteración flexibles, pero requieren mucho trabajo para definir acumuladores y funciones combinadoras. La clase de tipo Traverse es una herramienta de nivel superior que aprovecha Applicatives para proporcionar un patrón de iteración más conveniente y más legible.

Podemos demostrar Traverse utilizando los métodos Future.traverse y Future.sequence en la biblioteca estándar de Scala. Estos métodos proporcionan implementaciones específicas de Future del patrón poligonal. Como ejemplo, supongamos que tenemos una lista de servidores y un método para sondear un host por su tiempo de actividad:


import scala.concurrent._

import scala.concurrent.duration._

import scala.concurrent.ExecutionContext.Implicits.global

val hostnames = List(

    "alpha.example.com",

    "beta.example.com",

    "gamma.demo.com"

)

def getUptime(hostname: String): Future[Int] = Future(hostname.length * 60) // just for demonstration


Ahora, supongamos que queremos sondear todos los hosts y recopilar todos sus tiempos de actividad. No podemos simplemente asignar nombres de host porque el resultado, una Lista [Futuro [Int]], contendría más de un Futuro. Necesitamos reducir los resultados a un solo futuro para obtener algo que podamos bloquear. Comencemos haciendo esto manualmente usando un pliegue:


val allUptimes: Future[List[Int]] = hostnames.foldLeft(Future(List.empty[Int])) {

    (accum, host) =>

        val uptime = getUptime(host)

        for {

           accum  <- accum

           uptime <- uptime

        } yield accum :+ uptime

}

Await.result(allUptimes, 1.second)

// res0: List[Int] = List(1020, 960, 840)


Intuitivamente, iteramos sobre nombres de host, llamamos a func para cada elemento y combinamos los resultados en una lista. Esto suena simple, pero el código es bastante difícil de manejar debido a la necesidad de crear y combinar Futuros en cada iteración. Podemos mejorar mucho las cosas usando Future.traverse, que está hecho a medida para este patrón:


val allUptimes: Future[List[Int]] = Future.traverse(hostnames)(getUptime)

Await.result(allUptimes, 1.second)

// res2: List[Int] = List(1020, 960, 840)


Esto es mucho más claro y conciso. Veamos cómo funciona. Si ignoramos distracciones como CanBuildFrom y ExecutionContext, la implementación de Future.traverse en la biblioteca estándar se ve así:


def traverse[A, B](values: List[A])

(func: A => Future[B]): Future[List[B]] = values.foldLeft(Future(List.empty[B])) { 

    (accum, host) =>

        val item = func(host)

        for {

             accum <- accum

             item <- item

       } yield accum :+ item

}


Esto es esencialmente lo mismo que nuestro código de ejemplo anterior. Future.traverse está abstrayendo el dolor de plegar y definir acumuladores y funciones de combinación. Nos brinda una interfaz limpia de alto nivel para hacer lo que queramos:

  • empezar con una Lista[A];
  • proporcionar una función A => Futuro[B];
  • terminar con un Futuro[Lista[B]].
La biblioteca estándar también proporciona otro método, Future.sequence, que asume que comenzamos con List[Future[B]] y no necesitamos proporcionar una función de identidad:

object Future {
    def sequence[B](futures: List[Future[B]]): Future[List[B]] = traverse(futures)(identity)
// etc...
}

En este caso la comprensión intuitiva es aún más sencilla:

  • empezar con una Lista[Futuro[A]];
  • terminar con un Futuro[Lista[A]].
Future.traverse y Future.sequence resuelven un problema muy específico: nos permiten iterar sobre una secuencia de Futuros y acumular un resultado. Los ejemplos simplificados anteriores solo funcionan con Listas, pero el Future.traverse y Future.sequence funcionan con cualquier colección estándar de Scala.

La clase de tipo Traverse de Cats generaliza estos patrones para que funcionen con cualquier tipo de Aplicativo: Futuro, Opción, Validado, etc.