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]].
- empezar con una Lista[Futuro[A]];
- terminar con un Futuro[Lista[A]].