En la entrada anterior, Patrón Fodable en Cats, realicé una descripción de cómo se realizaban morfismos con tipos de datos algebraicos (ADT) utilizando la implementación del tipo Foldable de la librería cats. En la presente entrada, Patrón Traverse en Cats, me centraré en el tipo Traverse.
El tipo Traverse tiene dos funciones: traverse y sequence; en los siguientes apartados, realizaré la descripción de cada una.
1.- Traverse
El tipo Traverse define la función traverse la cual permite realizar lo siguiente: dado un tipo de entrada y dada una función de transformación; la función traverse permite: la iteración sobre el tipo de entrada, aplica la función a cada elemento de la entrada, acumular el resultado y retornar su resultado. Un ejemplo de una definición de función traverse puede ser el siguiente:
import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import cats.syntax.applicative._ import cats.syntax.apply._ def getFutureTest(msg: String): Future[Int] = Future{msg.length * 10 } def myTraverse[A,B](list: List[A])(f: A => Future[B]): Future[List[B]] = list.foldLeft(Future(List.empty[B])){ (acc, elem) => { val resultElem = f(elem) for{ acc <- acc elem <- resultElem }yield{ acc :+ elem } } } val listExample1 = List ("a", "aa", "aaa") val resultExample1 = myTraverse(listExample1)(getFutureTest) println(s"myTraverse(List ('a', 'aa', 'aaa'))-->${Await.result( resultExample1, 5.seconds )}")
La salida por consola es la siguiente:
myTraverse(List ('a', 'aa', 'aaa'))-->List(10, 20, 30)
El snippet anterior define lo siguiente: getFutureTest, función que retorna un Future de enteros que retorna la longitud del string pasado por parámetro multiplicado por 10; myTraverse, función traverse implementado con foldLeft la cual opera con una lista y una función f que retorna un Futuro del tipo B a partir del tipo A; listExample1, una lista de pruebas; y, por último, el mensaje con la función traverse y su visualización por pantalla.
1.1.- Traverse con Applicative
La función traverse podemos simplificarla utilizando tipos que cumplan el patrón Applicative la cual contiene operaciones del patrón Semigroupal como la función mapN; así, la función traverse, se puede redefinir de la siguiente manera:
def myTraverse2[F[_]: Applicative, A,B](list: List[A])(f: A => F[B]): F[List[B]] = list.foldLeft( List.empty[B].pure[F] ){ (acc, elem) => (acc, f(elem)).mapN(_ :+ _) } import cats.instances.option._ def process(list: List[Int]) = { myTraverse2(list)(n => if(n%2==0) Some(n) else None) } println(s"--Ejemplo3--") println(s"process(List(2,4,6))==>>${process(List(2,4,6))}") println(s"process(List(1,2,3))==>>${process(List(1,2,3))}")
La salida por consola es la siguiente:
process(List(2,4,6))==>>Some(List(2, 4, 6)) process(List(1,2,3))==>>None
1.2.- Traverse con Validated
El siguiente ejemplo, permite la validación de los elementos de una lista en función de un criterio: los elementos pares son válidos y, los impares, son inválidos. El snippet con la solución es la siguiente:
import cats.data.Validated import cats.instances.list._ type ErrorOn[A] = Validated[ List[String] ,A] def myTraverse2[F[_]: Applicative, A,B](list: List[A])(f: A => F[B]): F[List[B]] = list.foldLeft( List.empty[B].pure[F] ){ (acc, elem) => (acc, f(elem)).mapN(_ :+ _) } def process(list: List[Int]): ErrorOn[List[Int]] = { myTraverse2(list){ n => if(n%2==0){ Validated.valid(n) }else{ Validated.invalid(List(s"$n no está incluido.")) } } } println(s"process(List(2,4,6))==>>${process(List(2,4,6))}") println(s"process(List(1,2,3))==>>${process(List(1,2,3))}") println(s"process(List(2,4,5,6))==>>${process(List(2,4,5,6))}")
La salida por consola es la siguiente:
process(List(2,4,6))==>>Valid(List(2, 4, 6)) process(List(1,2,3))==>>Invalid(List(1 no está incluido., 3 no está incluido.)) process(List(2,4,5,6))==>>Invalid(List(5 no está incluido.))
1.3.- Función traverse con el tipo traverse
En los apartados anteriores, me he centrado en mostrar ejemplos de la función traverse con una implementación propia. En el siguiente ejemplo, muestro un ejemplo con la función traverse del tipo Traverse. La funcionalidad del ejemplo consiste en procesar una lista de futuros, el snippet es el siguiente:
import cats.Traverse import cats.instances.all._ val listExample1 = List ("a", "aa", "aaa") def getFutureTest(msg: String): Future[Int] = Future{msg.length * 10} val result1:Future[List[Int]] = Traverse[List].traverse(listExample1)(getFutureTest) println(s"Traverse1=${Await.result( result1, 2.seconds )}") val listExampleSequence1 = List( Future(1), Future(2), Future(3)) val result2: Future[List[Int]] = Traverse[List].sequence(listExampleSequence1) println(s"Sequence1=${Await.result( result2, 2.seconds )}")
La salida por consola es la siguiente:
Traverse1=List(10, 20, 30) Sequence1=List(1, 2, 3)
2.- Sequence
Por otro lado, el tipo Traverse define la función sequence la cual permite recorrer los elementos de un tipo de entrada y realizar los cambios de tipos. Un ejemplo de una función sequence puede ser la siguiente:
import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import cats.syntax.applicative._ import cats.syntax.apply._ def getFutureTest(msg: String): Future[Int] = Future{msg.length * 10 } def mySequence[B](list:List[Future[B]]): Future[List[B]] = myTraverse(list)(identity) val listExampleSequence1 = List( getFutureTest("a"), getFutureTest("aa"), getFutureTest("aaa")) val resultExample2 = mySequence(listExampleSequence1) println(s"myTraverse(List (Future('a'), Future('aa'), Future('aaa'))-->${Await.result( resultExample2, 5.seconds )}")
La salida por consola es la siguiente:
myTraverse(List (Future('a'), Future('aa'), Future('aaa'))-->List(10, 20, 30)
El snippet anterir define lo siguiente: getFutureTest, función que retorna un Future de enteros que retorna la longitud del string pasado por parámetro multiplicado por 10; mySequence, función que emplea la función traverse para realizar la transformación; listExampleSequence1, lista con los datos de prueba; y, por último, el mensaje con la función sequence y su visualización.
La funcionalidad del ejemplo anterior implemantado con la función sequence de Traverse queda descrito en el siguiente enjemplo:
import cats.Traverse import cats.instances.all._ val listExampleSequence1 = List( getFutureTest("a"), getFutureTest("aa"), getFutureTest("aaa")) val result1:Future[List[Int]] = Traverse[List].sequence(listExampleSequence1) println(s"myTraverse(List (Future('a'), Future('aa'), Future('aaa'))-->${Await.result( result1, 5.seconds )}")
La salida por consola es la siguiente:
myTraverse(List (Future('a'), Future('aa'), Future('aaa'))-->List(10, 20, 30)
3.- Definición formal de Traverse
La definición formal del trait con la funcionalidad Traverse es la siguiente:
package cats trait Traverse[F[_]] { def traverse[G[_]: Applicative, A, B] (inputs: F[A])(func: A => G[B]): G[F[B]] def sequence[G[_]: Applicative, B] (inputs: F[G[B]]): G[F[B]] = traverse(inputs)(identity) }
Para finalizar la entrada y como conclusión final, el tipo Traverse es un patrón conseguido y comprensible a partir del patrón Foldable y la función fold. Traverse permite realizar la iteración y operación sobre colecciones de tipos y, además, realizar acumuladores de resultados de dichas colecciones.