Mostrando las entradas con la etiqueta Cats. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Cats. Mostrar todas las entradas

lunes, 5 de junio de 2023

Mi experiencia con la programación funcional en Scala


Creo que si leen este blog saben que me encanta la programación funcional y investigar y aprender. Y en eso estoy...


Me gustaría contar un poco mi evolución y mis altibajos para que la gente que sabe me aconseje y los que no saben puedan aprender de mis exitos y derrotas. 


Bueno, todo empezó con un curso de scala de coursera y quede encantado con el lenguaje y la programación funcional. De a poco tambien me interiorice en otros lenguajes como erlang, haskell, Clojure, Elixir, F#, etc ... pero siempre en la volvía a Scala por gusto nomas. Y me iba bastante bien...


Hasta que empece a quedarme corto en scala, quería progresar y decidí ver algunos framework funcionales, leí un libro de Cats y la verdad es que si bien esta bueno, me frustre mucho... porque no sabia porque se hacían ciertas cosas, no entendía porque tan complejo o si bien entendía desconfiaba ( no hay otra forma de hacerlo más fácil) y bueno ... 


Cambie a Akka, y me pareció bueno y me sentí más en mi mundo, pero justo justo cuando volvia el entuciasmo, cambio de licencia y bueno... Se fue el entusiasmo, ahora sé que existe un fork open source llamado pekko (https://github.com/apache/incubator-pekko) y es de apache. Pero lo veo verde, y no sé cuanto va a crecer ...


Y ahora estoy con ZIO y por ahora todo va bien, por ende la conclusión es si quieren progresar en su conocimiento en Scala, y no vienen del mundo funcional como Haskell, ZIO esta bueno. 

Si son nuevos en el mundo funcional y scala, yo haría lo siguiente: 

1. Estudiar Scala (curso de scala de coursera, el libro rojo de programación funcional en scala, etc ...)

2. Leer y estudiar ZIO 

3. Leer y estudiar Cats


Pero no perder de vista Pekko, para ver como progresa. 


Que opinan? 


lunes, 13 de marzo de 2023

Vamos a implementar un framework de procesamiento paralelo simple con Cats parte 2

Dado que en el post anterios refrescamos nuestra memoria de Futures, veamos cómo podemos dividir el trabajo en lotes. Podemos consultar la cantidad de CPU disponibles en nuestra máquina mediante una llamada API desde la biblioteca estándar de Java:

Runtime.getRuntime.availableProcessors

// res11: Int = 2

Podemos particionar una secuencia (en realidad cualquier cosa que implemente Vector) usando el método agrupado. Usaremos esto para dividir lotes de trabajo para cada CPU:


(1 to 10).toList.grouped(3).toList

// res12: List[List[Int]] = List(

//List(1, 2, 3),

//List(4, 5, 6),

//List(7, 8, 9),

//List(10)

// )


Implemente una versión paralela de foldMap llamada parallelFoldMap:


def parallelFoldMap[A, B: Monoid] (values: Vector[A]) (func: A => B): Future[B] = {

    // Calculate the number of items to pass to each CPU:

    val numCores = Runtime.getRuntime.availableProcessors

    val groupSize = (1.0 * values.size / numCores).ceil.toInt

    // Create one group for each CPU:

    val groups: Iterator[Vector[A]] = values.grouped(groupSize)

    // Create a future to foldMap each group:

    val futures: Iterator[Future[B]] =

        groups map { group =>

            Future {

                group.foldLeft(Monoid[B].empty)(_ |+| func(_))

            }

    }


    // foldMap over the groups to calculate a final result:

    Future.sequence(futures) map { iterable =>

    iterable.foldLeft(Monoid[B].empty)(_ |+| _)

    }

}

val result: Future[Int] = parallelFoldMap((1 to 1000000).toVector)(identity)

Await.result(result, 1.second)

// res14: Int = 1784293664


Aunque implementamos foldMap nosotros mismos arriba, el método también está disponible como parte de la clase de tipo Foldable:


import cats.Monoid

import cats.instances.int._

// for Monoid

import cats.instances.future._ // for Applicative and Monad

import cats.instances.vector._ // for Foldable and Traverse

import cats.syntax.foldable._// for combineAll and foldMap

import cats.syntax.traverse._// for traverse

import scala.concurrent._

import scala.concurrent.duration._

import scala.concurrent.ExecutionContext.Implicits.global


def parallelFoldMap[A, B: Monoid](values: Vector[A]) (func: A => B): Future[B] = {

    val numCores = Runtime.getRuntime.availableProcessors

    val groupSize = (1.0 * values.size / numCores).ceil.toInt

    values.grouped(groupSize).toVector.traverse(group => Future(group.toVector.foldMap(func)))

        .map(_.combineAll)

}

val future: Future[Int] = parallelFoldMap((1 to 1000).toVector)(_ * 1000)

Await.result(future, 1.second)

// res18: Int = 500500000

Vamos a implementar un framework de procesamiento paralelo simple con Cats

Vamos a implementar un framework de procesamiento paralelo simple pero poderoso utilizando Monoids, Functors y una serie de otras cosas.

Si ha utilizado Hadoop o Spark o ha trabajado en "grandes datos", habrá oído hablar de MapReduce, que es un modelo de programación para realizar el procesamiento de datos en paralelo en grupos de máquinas (también conocidos como "nodos"). Como sugiere el nombre, el modelo se construye alrededor de una fase de map, que es la misma función de map que conocemos de Scala y la clase de tipo Functor, y una fase de reducción, que generalmente llamamos plegar o fold en Scala.

Recuerde que la firma general para el mapa es aplicar una función A => B a una F[A], devolviendo una F[B]

map transforma cada elemento individual en una secuencia de forma independiente. Podemos paralelizar fácilmente porque no hay dependencias entre las transformaciones aplicadas a diferentes elementos (la firma de tipo de la función A => B).

Nuestro paso de reducción se convierte en un pliegue a la izquierda sobre los resultados del map distribuido.

Al distribuir el paso de reducción, perdemos el control sobre el orden de recorrido. Es posible que nuestra reducción general no sea completamente de izquierda a derecha: podemos reducir de izquierda a derecha en varias subsecuencias y luego combinar los resultados. Para asegurar la corrección necesitamos una operación de reducción que sea asociativa:

reduce(a1, reduce(a2, a3)) == reduce(reduce(a1, a2), a3)

Si tenemos asociatividad, podemos distribuir arbitrariamente el trabajo entre nuestros nodos siempre que las subsecuencias en cada nodo permanezcan en el mismo orden que el conjunto de datos inicial.

Nuestra operación de plegado nos obliga a sembrar el cálculo con un elemento de tipo B. Dado que el plegado se puede dividir en un número arbitrario de pasos paralelos, la semilla no debería afectar el resultado del cálculo. Esto naturalmente requiere que la semilla sea un elemento de identidad:

reduce(seed, a1) == reduce(a1, seed) == a1

En resumen, nuestro pliegue paralelo dará los resultados correctos si:

• requerimos que la función reductora sea asociativa;

• Sembramos el cálculo con la identidad de esta función.

¿Cómo suena este patrón? Así es, hemos completado el círculo de regreso a Monoid. El patrón de diseño monoide para trabajos de reducción de mapas está en el centro de los sistemas de big data recientes, como Summingbird de Twitter.

En este proyecto vamos a implementar un map-reduce de una sola máquina muy simple. Comenzaremos implementando un método llamado foldMap para modelar el flujo de datos que necesitamos.

Vimos foldMap es una de las operaciones derivadas que se encuentra encima de foldLeft y foldRight. Sin embargo, en lugar de usar Foldable, volveremos a implementar foldMap aquí nosotros mismos, ya que proporcionará información útil sobre la estructura de map‐reduce. Comience escribiendo la firma de foldMap. Debe aceptar los siguientes parámetros:

• una secuencia de tipo Vector[A];

• una función de tipo A => B, donde hay un Monoide para B;

Deberá agregar parámetros implícitos o límites de contexto para completar la firma de tipo.

import cats.Monoid

/** Single-threaded map-reduce function.

* Maps `func` over `values` and reduces using a `Monoid[B]`.

*/

def foldMap[A, B: Monoid](values: Vector[A])(func: A => B): B =

???

Ahora implemente el cuerpo de foldMap:

  1. comenzar con una secuencia de elementos de tipo A;
  2. mapear sobre la lista para producir una secuencia de elementos de tipo B;
  3. usa el Monoide para reducir los elementos a una sola B.

Aquí hay algunos resultados de muestra para referencia:


import cats.instances.int._ // for Monoid

foldMap(Vector(1, 2, 3))(identity)

// res1: Int = 6

import cats.instances.string._ // for Monoid

// Mapping to a String uses the concatenation monoid:

foldMap(Vector(1, 2, 3))(_.toString + "! ")

// res2: String = "1! 2! 3! "

// Mapping over a String to produce a String:

foldMap("Hello world!".toVector)(_.toString.toUpperCase)

// res3: String = "HELLO WORLD!"


Veamos la implementación: 


import cats.Monoid

import cats.syntax.semigroup._ // for |+|

def foldMap[A, B : Monoid](as: Vector[A])(func: A => B): B =

as.map(func).foldLeft(Monoid[B].empty)(_ |+| _)


Podemos hacer una pequeña alteración a este código para hacer todo en un solo paso:


def foldMap[A, B : Monoid](as: Vector[A])(func: A => B): B =

as.foldLeft(Monoid[B].empty)(_ |+| func(_))


Ahora que tenemos una implementación funcional de subproceso único de foldMap, veamos cómo distribuir el trabajo para que se ejecute en paralelo. Usaremos nuestra versión de subproceso único de foldMap como bloque de construcción. Escribiremos una implementación de CPU múltiple que simule la forma en que distribuiríamos el trabajo en un clúster de reducción de mapa:

  1. comenzamos con una lista inicial de todos los datos que necesitamos procesar;
  2. dividimos los datos en lotes, enviando un lote a cada CPU;
  3. las CPU ejecutan una fase de mapa a nivel de lote en paralelo;
  4. Las CPU ejecutan una fase de reducción de nivel de lote en paralelo, produciendo un local resultado de cada lote;
  5. reducimos los resultados de cada lote a un único resultado final.

Scala proporciona algunas herramientas simples para distribuir el trabajo entre subprocesos. Podríamos usar la biblioteca de colecciones paralelas para implementar una solución, pero desafiémonos a nosotros mismos profundizando un poco más e implementando el algoritmo nosotros mismos usando Futures.

Ya sabemos bastante sobre la naturaleza monádica de Futures. Tomemos un momento para un resumen rápido y para describir cómo se programan los futuros de Scala detrás de escena.

Los futuros se ejecutan en un grupo de subprocesos, determinado por un parámetro ExecutionContext implícito.

Cada vez que creamos un futuro, ya sea a través de una llamada a Future.apply o algún otro combinador, debemos tener un ExecutionContext implícito en el alcance:

import scala.concurrent.Future

import scala.concurrent.ExecutionContext.Implicits.global

val future1 = Future {

     (1 to 100).toList.foldLeft(0)(_ + _)

}

// future1: Future[Int] = Future(Success(5050))

val future2 = Future {

    (100 to 200).toList.foldLeft(0)(_ + _)

}

// future2: Future[Int] = Future(Success(15150))


En este ejemplo, hemos importado un ExecutionContext.Implicits.global. Este contexto predeterminado asigna un grupo de subprocesos con un subproceso por CPU en nuestra máquina. Cuando creamos un futuro, ExecutionContext lo programa para su ejecución. Si hay un subproceso libre en el grupo, Future comienza a ejecutarse de inmediato. La mayoría de las máquinas modernas tienen al menos dos CPU, por lo que en nuestro ejemplo es probable que future1 y future2 se ejecuten en paralelo.

Algunos combinadores crean nuevos Futuros que programan el trabajo en función de los resultados de otros Futuros. Los métodos map y flatMap, por ejemplo, programan cálculos que se ejecutan tan pronto como se calculan sus valores de entrada y hay una CPU disponible:

val future3 = future1.map(_.toString)

// future3: Future[String] = Future(Success(5050))

val future4 = for {

    a <- future1

    b <- future2

} yield a + b

// future4: Future[Int] = Future(Success(20200))


Podemos convertir una List[Future[A]] en una Future[List[A]] usando Future.sequence:


Future.sequence(List(Future(1), Future(2), Future(3)))

// res6: Future[List[Int]] = Future(Success(List(1, 2, 3)))


Podemos convertir una List[Future[A]] en una Future[List[A]] usando Future.sequence:


import cats.instances.future._ // for Applicative

import cats.instances.list._// for Traverse

import cats.syntax.traverse._// for sequence

List(Future(1), Future(2), Future(3)).sequence

// res7: Future[List[Int]] = Future(Success(List(1, 2, 3)))


Se requiere un ExecutionContext en cualquier caso. Finalmente, podemos usar Await.result para bloquear un futuro hasta que haya un resultado disponible:


import scala.concurrent._

import scala.concurrent.duration._

Await.result(Future(1), 1.second) // wait for the result

// res8: Int = 1


También hay implementaciones de Monad y Monoid para Future disponibles en cats.instances.future:


import cats.{Monad, Monoid}

import cats.instances.int._

// for Monoid

import cats.instances.future._ // for Monad and Monoid

Monad[Future].pure(42)

Monoid[Future[Int]].combine(Future(1), Future(2))


Me quedo medio largo el post, así que vamos a seguir en el proximo post. 



martes, 21 de febrero de 2023

Probar código asíncrono con cats

Cómo simplificar las pruebas unitarias para código asincrónico haciéndolas sincrónicas con Cats? Esta es la pregunta que vamos a intantar responder. 

Supongamos que estamos midiendo el tiempo de actividad en un conjunto de servidores.  Habrá dos componentes. El primero es un UptimeClient que sondea servidores remotos para conocer su tiempo de actividad:


import scala.concurrent.Future

trait UptimeClient {

    def getUptime(hostname: String): Future[Int]

}


También tendremos un UptimeService que mantiene una lista de servidores y permite al usuario sondearlos por su tiempo de actividad total:


import cats.instances.future._ // for Applicative

import cats.instances.list._// for Traverse

import cats.syntax.traverse._// for traverse

import scala.concurrent.ExecutionContext.Implicits.global


class UptimeService(client: UptimeClient) {

    def getTotalUptime(hostnames: List[String]): Future[Int] =

        hostnames.traverse(client.getUptime).map(_.sum)

}

Hemos modelado UptimeClient como un trait porque vamos a querer probarlo en pruebas unitarias. Por ejemplo, podemos escribir un cliente de prueba que nos permita proporcionar datos ficticios en lugar de llamar a servidores reales:


class TestUptimeClient(hosts: Map[String, Int]) extends UptimeClient {

    def getUptime(hostname: String): Future[Int] =

        Future.successful(hosts.getOrElse(hostname, 0))

}


Ahora, supongamos que estamos escribiendo pruebas unitarias para UptimeService. Queremos probar su capacidad para sumar valores, independientemente de dónde los obtenga. Aquí hay un ejemplo:


def testTotalUptime() = {

   val hosts= Map("host1" -> 10, "host2" -> 6)

   val client= new TestUptimeClient(hosts)

   val service= new UptimeService(client)

   val actual= service.getTotalUptime(hosts.keys.toList)

   val expected = hosts.values.sum

   assert(actual == expected)

}


El código no compila porque hemos cometido un error clásico. Olvidamos que nuestro código de aplicación es asíncrono. Nuestro resultado real es de tipo Future[Int] y nuestro resultado esperado es de tipo Int. ¡No podemos compararlos directamente!

Hay un par de maneras de resolver este problema. Podríamos modificar nuestro código de prueba para acomodar la asincronía. Sin embargo, existe otra alternativa. ¡Hagamos que nuestro código de servicio sea sincrónico para que nuestra prueba funcione sin modificaciones!

Necesitamos implementar dos versiones de UptimeClient: una asíncrona para usar en producción y una síncrona para usar en nuestras pruebas unitarias:


trait RealUptimeClient extends UptimeClient {

    def getUptime(hostname: String): Future[Int]

}

trait TestUptimeClient extends UptimeClient {

    def getUptime(hostname: String): Int

}


La pregunta es: ¿qué tipo de resultado debemos dar al método abstracto en UptimeClient? Future[Int] o Int:


trait UptimeClient {

    def getUptime(hostname: String): ???

}


Al principio esto puede parecer difícil. Pero afortunadamente, Cats brinda una solución en términos del tipo de identidad, Id. Id nos permite "envolver" tipos en un constructor de tipos sin cambiar su significado:


package cats

type Id[A] = A


Id nos permite abstraernos sobre los tipos de devolución en UptimeClient. Implementa esto ahora:


import cats.Id

trait UptimeClient[F[_]] {

    def getUptime(hostname: String): F[Int]

}

trait RealUptimeClient extends UptimeClient[Future] {

   def getUptime(hostname: String): Future[Int]

}

trait TestUptimeClient extends UptimeClient[Id] {

   def getUptime(hostname: String): Id[Int]

}


Ahora deberíamos poder desarrollar una definición de TestUptimeClient en una clase completa basada en Map[String, Int].


Dirijamos nuestra atención a UptimeService. Necesitamos reescribirlo para abstraer los dos tipos de UptimeClient:

class UptimeService[F[_]](client: UptimeClient[F]) {

    def getTotalUptime(hostnames: List[String]): F[Int] =

        hostnames.traverse(client.getUptime).map(_.sum)

}

Ahora descomente el cuerpo de getTotalUptime. Debería obtener un error de compilación similar al siguiente:


// <console>:28: error: could not find implicit value for

//evidence parameter of type cats.Applicative[F]

//hostnames.traverse(client.getUptime).map(_.sum)

//                              ^


El problema aquí es que traverse solo funciona en secuencias de valores que tienen un Aplicativo. En nuestro código original estábamos atravesando una Lista[Futuro[Int]]. Hay un aplicativo para Future, así que estuvo bien.

En esta versión estamos atravesando una Lista[F[Int]]. Necesitamos demostrarle al compilador que F tiene un Aplicativo. Haga esto agregando un parámetro de constructor implícito a UptimeService.


import cats.Applicative

import cats.syntax.functor._ // for map


class UptimeService[F[_]](client: UptimeClient[F]) (implicit a: Applicative[F]) {

    def getTotalUptime(hostnames: List[String]): F[Int] =

        hostnames.traverse(client.getUptime).map(_.sum)

}


Finalmente, dirijamos nuestra atención a nuestras pruebas unitarias. Nuestro código de prueba ahora funciona según lo previsto sin ninguna modificación. Creamos una instancia de TestUptimeClient y la envolvemos en un UptimeService. Esto une efectivamente F a Id, lo que permite que el resto del código funcione sincrónicamente sin preocuparse por las mónadas o los aplicativos:


def testTotalUptime() = {

    val hosts= Map("host1" -> 10, "host2" -> 6)

    val client= new TestUptimeClient(hosts)

    val service= new UptimeService(client)

    val actual= service.getTotalUptime(hosts.keys.toList)

    val expected = hosts.values.sum

    assert(actual == expected)

}

testTotalUptime()


Este estudio de caso proporciona un ejemplo de cómo Cats puede ayudarnos a abstraernos en diferentes escenarios computacionales. Usamos la clase de tipo Applicative para abstraer sobre código asíncrono y síncrono. Apoyarnos en una abstracción funcional nos permite especificar la secuencia de cálculos que queremos realizar sin preocuparnos por los detalles de la implementación.

Las clases de tipo como Functor, Applicative, Monad y Traverse proporcionan implementaciones abstractas de patrones como mapeo, compresión, secuenciación e iteración. Las leyes matemáticas de esos tipos aseguran que funcionen junto con un conjunto consistente de semántica.

Usamos Applicative en este caso de estudio porque era la clase de tipos menos poderosa que hacía lo que necesitábamos. Si hubiéramos requerido flatMap, podríamos haber cambiado Applicative por Monad. Si hubiéramos necesitado abstraernos sobre diferentes tipos de secuencias, podríamos haber usado Traverse. También hay clases de tipos como ApplicativeError y MonadError que ayudan a modelar fallas y cálculos exitosos.

sábado, 4 de febrero de 2023

Foldable y Traverse parte 7

Nuestros métodos listTraverse y listSequence funcionan con cualquier tipo de Aplicativo, pero solo funcionan con un tipo de secuencia: List. Podemos generalizar sobre diferentes tipos de secuencias utilizando una type class, lo que nos lleva a Cats' Traverse. Aquí está la definición abreviada:

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)

}

Cats proporciona instancias de Poligonal para List, Vector, Stream, Option, Either y una variedad de otros tipos. Podemos invocar instancias como de costumbre usando Traverse.apply y usar los métodos poligonal y de secuencia como se describe en la sección anterior:


import cats.Traverse

import cats.instances.future._ // for Applicative

import cats.instances.list._


// for Traverse

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

Await.result(totalUptime, 1.second)

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


val numbers = List(Future(1), Future(2), Future(3))

val numbers2: Future[List[Int]] = Traverse[List].sequence(numbers)

Await.result(numbers2, 1.second)

// res1: List[Int] = List(1, 2, 3)


También hay versiones sintácticas de los métodos, importadas a través de cats.syntax.traverse:


import cats.syntax.traverse._ // for sequence and traverse

Await.result(hostnames.traverse(getUptime), 1.second)

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

Await.result(numbers.sequence, 1.second)

// res3: List[Int] = List(1, 2, 3)


Como puede ver, ¡es mucho más compacto y legible que el código foldLeft con el que comenzamos al principio!

Foldable y Traverse parte 6

Podemos reescribir la poligonal en términos de un Aplicativo. Nuestro acumulador del ejemplo anterior:


Future(List.empty[Int])


es equivalente a Applicative.pure:


import cats.Applicative

import cats.instances.future._ // for Applicative

import cats.syntax.applicative._ // for pure

List.empty[Int].pure[Future]


Nuestro combinador, que solía ser este:


def oldCombine(accum : Future[List[Int]],host: String): Future[List[Int]] = {

val uptime = getUptime(host)

for {

        accum <- accum

        uptime <- uptime

    } yield accum :+ uptime

}


ahora es equivalente a Semigroupal.combine:


import cats.syntax.apply._ // for mapN

// Combining accumulator and hostname using an Applicative:

def newCombine(accum: Future[List[Int]], host: String): Future[List[Int]] =

(accum, getUptime(host)).mapN(_ :+ _)


Al sustituir estos fragmentos en la definición de poligonal, podemos generalizarla para que funcione con cualquier aplicativo:


def listTraverse[F[_]: Applicative, A, B] (list: List[A])(func: A => F[B]): F[List[B]] =

list.foldLeft(List.empty[B].pure[F]) { (accum, item) =>

(accum, func(item)).mapN(_ :+ _)

}


def listSequence[F[_]: Applicative, B] (list: List[F[B]]): F[List[B]] =

listTraverse(list)(identity)


Podemos usar listTraverse para volver a implementar nuestro ejemplo de tiempo de actividad:


val totalUptime = listTraverse(hostnames)(getUptime)

Await.result(totalUptime, 1.second)

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


o podemos usarlo con otros tipos de datos de Applicative.



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. 

sábado, 7 de enero de 2023

Foldable y Traverse parte 4

Cada método en Foldable está disponible en forma de sintaxis a través de cats.syntax.foldable. En cada caso, el primer argumento del método en Foldable se convierte en el receptor de la llamada al método:


import cats.syntax.foldable._ // for combineAll and foldMap

List(1, 2, 3).combineAll

// res12: Int = 6

List(1, 2, 3).foldMap(_.toString)

// res13: String = "123"


Recuerde que Scala solo usará una instancia de Foldable si el método no está explícitamente disponible en el receptor. Por ejemplo, el siguiente código usará la versión de foldLeft definida en List:


List(1, 2, 3).foldLeft(0)(_ + _)

// res14: Int = 6


mientras que el siguiente código genérico utilizará Foldable:


def sum[F[_]: Foldable](values: F[Int]): Int = values.foldLeft(0)(_ + _)


Por lo general, no necesitamos preocuparnos por esta distinción. ¡Es una característica! Llamamos al método que queremos y el compilador usa un Foldable cuando es necesario para garantizar que nuestro código funcione como se espera. Si necesitamos una implementación segura de pila de foldRight, usar Eval como acumulador es suficiente para obligar al compilador a seleccionar el método de Cats.


martes, 27 de diciembre de 2022

Foldable y Traverse parte 3

 Foldable define foldRight de manera diferente a foldLeft, en términos de la mónada Eval:


def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B]


El uso de Eval significa que el plegado siempre es seguro para la pila, incluso cuando la definición predeterminada de foldRight de la colección no lo es. Por ejemplo, la implementación predeterminada de foldRight para LazyList no es segura a nivel de pila. Cuanto más larga sea la lista perezosa, mayores serán los requisitos de pila para el pliegue. Una lista perezosa lo suficientemente grande provocará un StackOverflowError:


import cats.Eval

import cats.Foldable

def bigData = (1 to 100000).to(LazyList)

bigData.foldRight(0L)(_ + _)

// java.lang.StackOverflowError ...


El uso de Foldable nos obliga a usar operaciones seguras de pila, lo que corrige la excepción de desbordamiento:


import cats.instances.lazyList._ // for Foldable

val eval: Eval[Long] =

    Foldable[LazyList].

        foldRight(bigData, Eval.now(0L)) { (num, eval) =>

            eval.map(_ + num)

        }

eval.value

// res3: Long = 5000050000L


Foldable nos proporciona una gran cantidad de métodos útiles definidos en la parte superior de foldLeft. Muchos de estos son facsímiles de métodos familiares de la biblioteca estándar: find, exist, forall, toList, isEmpty, nonEmpty, etc.

Además de estos métodos familiares, Cats proporciona dos métodos que hacen uso de Monoids:

  • combineAll (y su alias fold) combina todos los elementos en la secuencia usando su Monoid;
  • foldMap mapea una función proporcionada por el usuario sobre la secuencia y combina los resultados usando un Monoid.

Por ejemplo, podemos usar combineAll para sumar sobre List[Int]:


import cats.instances.int._ // for Monoid

Foldable[List].combineAll(List(1, 2, 3))

// res8: Int = 6


Alternativamente, podemos usar foldMap para convertir cada Int en un String y concatenarlos:


import cats.instances.string._ // for Monoid

Foldable[List].foldMap(List(1, 2, 3))(_.toString)

// res9: String = "123"


Finalmente, podemos componer Foldables para admitir un recorrido profundo de secuencias anidadas:


import cats.instances.vector._ // for Monoid

val ints = List(Vector(1, 2, 3), Vector(4, 5, 6))

(Foldable[List] compose Foldable[Vector]).combineAll(ints)

// res11: Int = 21


lunes, 26 de diciembre de 2022

Foldable y Traverse parte 2

Foldable en cats es un type class que contiene el método foldLeft y foldRight. Las instancias de Foldable definen estos dos métodos y heredan una serie de métodos derivados. Cats proporciona instancias listas para usar de Foldable para un puñado de tipos de datos de Scala: List, Vector, LazyList y Option.

Podemos invocar instancias como de costumbre usando Foldable.apply y llamar directamente a sus implementaciones de foldLeft. Aquí hay un ejemplo usando List:


import cats.Foldable

import cats.instances.list._ // for Foldable

val ints = List(1, 2, 3)

Foldable[List].foldLeft(ints, 0)(_ + _)

// res0: Int = 6


Otras secuencias como Vector y LazyList funcionan de la misma manera. Aquí hay un ejemplo usando Option, que se trata como una secuencia de cero o uno elementos:


import cats.instances.option._ // for Foldable

val maybeInt = Option(123)

Foldable[Option].foldLeft(maybeInt, 10)(_ * _)

// res1: Int = 1230



martes, 20 de diciembre de 2022

Foldable y Traverse

La clase de tipo Foldable captura los métodos foldLeft y foldRight a los que estamos acostumbrados en secuencias como Lists, Vectors, y Streams. Usando Foldable, podemos escribir pliegues genéricos que funcionan con una variedad de tipos de secuencias. También podemos inventar nuevas secuencias y conectarlas a nuestro código.

Foldable nos brinda excelentes casos de uso para Monoids y la mónada Eval.

Comencemos con un resumen rápido del concepto general de plegado. Suministramos un valor acumulador y una función binaria para combinarlo con cada elemento de la secuencia:


def show[A](list: List[A]): String =

    list.foldLeft("nil")((accum, item) => s"$item then $accum")

show(Nil)

// res0: String = "nil"

show(List(1, 2, 3))

// res1: String = "3 then 2 then 1 then nil"


El método foldLeft funciona recursivamente en la secuencia. Nuestra función binaria se llama repetidamente para cada elemento, y el resultado de cada llamada se convierte en el acumulador de la siguiente. Cuando llegamos al final de la secuencia, el acumulador final se convierte en nuestro resultado final.

Dependiendo de la operación que estemos realizando, el orden en el que plegamos puede ser importante. Debido a esto, hay dos variantes estándar de pliegue:

  • foldLeft atraviesa de "izquierda" a "derecha" (de principio a fin);
  • foldRight atraviesa de "derecha" a "izquierda" (de fin a principio).

foldLeft y foldRight son equivalentes si nuestra operación binaria es asociativa.

Por ejemplo, podemos sumar List[Int] doblando en cualquier dirección, usando 0 como nuestro acumulador y la suma como nuestra operación pero no podemos hacer lo mismo con la resta :


List(1, 2, 3).foldLeft(0)(_ - _)

// res4: Int = -6

List(1, 2, 3).foldRight(0)(_ - _)

// res5: Int = 2




martes, 13 de diciembre de 2022

Semigroupal, Parallel y Applicative parte 8


Con la introducción de Apply y Applicative, podemos alejarnos y ver toda una familia de clases de tipos que se ocupan de secuenciar cálculos de diferentes maneras. 

Cada clase de tipo en la jerarquía representa un conjunto particular de semántica de secuenciación, presenta un conjunto de métodos característicos y define la funcionalidad de sus supertipos en términos de ellos:

  • toda mónada es un aplicativo;
  • cada aplicativo un semigrupal;
  • y así.

Debido a la naturaleza de las relaciones entre las type class, las relaciones de herencia son constantes en todas las instancias.

Apply define product en términos de ap y map; Monad define product, ap y map, en términos de  pure y flatMap.

Para ilustrar esto, consideremos dos tipos de datos hipotéticos:

• Foo es una mónada. Tiene una instancia de la type class Monad que implementa pure y flatMap y hereda definiciones estándar de product, map y ap;

• Bar es un funtor aplicativo. Tiene una instancia de Applicative que implementa pure y ap y hereda definiciones estándar de product y map.

¿Qué podemos decir sobre estos dos tipos de datos sin saber más sobre su implementación?

Sabemos estrictamente más sobre Foo que sobre Bar: Monad es un subtipo de Applicative, por lo que podemos garantizar propiedades de Foo (a saber, flatMap) que no podemos garantizar con Bar. Por el contrario, sabemos que Bar puede tener una gama más amplia de comportamientos que Foo. Tiene menos leyes que obedecer (sin flatMap), por lo que puede implementar comportamientos que Foo no puede.

Esto demuestra el clásico intercambio de poder (en el sentido matemático) versus restricción. Cuantas más restricciones imponemos a un tipo de datos, más garantías tenemos sobre su comportamiento, pero menos comportamientos podemos modelar.

Las mónadas resultan ser un punto dulce en esta compensación. Son lo suficientemente flexibles para modelar una amplia gama de comportamientos y lo suficientemente restrictivos para dar garantías sólidas sobre esos comportamientos. Sin embargo, hay situaciones en las que las mónadas no son la herramienta adecuada para el trabajo. A veces queremos comida tailandesa y los burritos simplemente no satisfacen.

Mientras que las mónadas imponen una secuencia estricta en los cálculos que modelan, los aplicativos y los semigrupos no imponen tal restricción. Esto los coloca en un punto dulce diferente en la jerarquía. Podemos usarlos para representar clases de cálculos paralelos/independientes que las mónadas no pueden.

Elegimos nuestra semántica eligiendo nuestras estructuras de datos. Si elegimos una mónada, obtenemos una secuencia estricta. Si elegimos un aplicativo, perdemos la capacidad de flatMap. Esta es la compensación impuesta por las leyes de consistencia. ¡Así que elige tus tipos con cuidado!

Si bien las mónadas y los funtores son los tipos de datos de secuenciación más utilizados, los semigrupos y los aplicativos son los más generales.

Estas clases de tipos proporcionan un mecanismo genérico para combinar valores y aplicar funciones dentro de un contexto, a partir del cual podemos crear mónadas y una variedad de otros combinadores.

Semigroupal y Applicative se usan más comúnmente como un medio para combinar valores independientes, como los resultados de las reglas de validación. Cats proporciona el tipo Validated para este propósito específico, junto con la sintaxis de aplicación como una forma conveniente de expresar la combinación de reglas.

lunes, 12 de diciembre de 2022

Semigroupal, Parallel y Applicative parte 7

Los semigrupos no se mencionan con frecuencia en la literatura más amplia de programación funcional. Proporcionan un subconjunto de la funcionalidad de un type class relacionada llamada funtor aplicativo ("aplicativo" para abreviar).

Semigroupal y Applicative proporcionan codificaciones alternativas de la misma noción de unir contextos. Ambas codificaciones se presentan en el mismo artículo de 2008 de Conor McBride y Ross Paterson.

Cats modela applicatives usando dos type classes. El primero, cats.Apply, extiende Semigroupal y Functor y agrega un método ap que aplica un parámetro a una función dentro de un contexto. El segundo, cats.Applicative, extiende Apply y agrega el método  pure. Aquí hay una definición simplificada en el código:


trait Apply[F[_]] extends Semigroupal[F] with Functor[F] {

    def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

    def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] =

         ap(map(fa)(a => (b: B) => (a, b)))(fb)

}

trait Applicative[F[_]] extends Apply[F] { 

     def pure[A](a: A): F[A]

}

Desglosando esto, el método ap aplica un parámetro fa a una función ff dentro de un contexto F[_]. El método product de Semigroupal se define en términos de ap y map.

No se preocupe demasiado por la implementación de product: es difícil de leer y los detalles no son particularmente importantes. El punto principal es que existe una estrecha relación entre product, ap y map que permite definir cualquiera de ellos en términos de los otros dos.

El Applicative también introduce el método pure. Este es el mismo pure que vimos en Monad. Construye una nueva instancia de aplicación a partir de un valor no encapsulado. En este sentido, Applicative está relacionado con Apply como Monoid está relacionado con Semigroup.



miércoles, 7 de diciembre de 2022

Semigroupal, Parallel y Applicative parte 6

Cuando llamamos a product en un tipo que tiene una instancia de Monad, obtenemos una semántica secuencial. Esto tiene sentido desde el punto de vista de mantener la coherencia con las implementaciones de product en términos de flatMap y map. Sin embargo, no siempre es lo que queremos. La clase de tipo Parallel, y su sintaxis asociada, nos permite acceder a semánticas alternativas para ciertas mónadas.

Hemos visto cómo el método del producto en Either se detiene en el primer error.


import cats.Semigroupal

import cats.instances.either._ // for Semigroupal

type ErrorOr[A] = Either[Vector[String], A]

val error1: ErrorOr[Int] = Left(Vector("Error 1"))

val error2: ErrorOr[Int] = Left(Vector("Error 2"))

Semigroupal[ErrorOr].product(error1, error2)

// res0: ErrorOr[(Int, Int)] = Left(Vector("Error 1"))


También podemos escribir esto usando tupled como atajo.


import cats.syntax.apply._ // for tupled

import cats.instances.vector._ // for Semigroup on Vector

(error1, error2).tupled

// res1: ErrorOr[(Int, Int)] = Left(Vector("Error 1"))


Para recopilar todos los errores simplemente reemplazamos tupled con su versión “paralela” llamada parTupled.


import cats.syntax.parallel._ // for parTupled

(error1, error2).parTupled

// res2: ErrorOr[(Int, Int)] = Left(Vector("Error 1", "Error 2"))


¡Se devuelven ambos errores! Este comportamiento no es especial para usar Vector como tipo de error. Cualquier tipo que tenga una instancia de Semigroup funcionará.

Por ejemplo, aquí usamos List en su lugar.


import cats.instances.list._ // for Semigroup on List

type ErrorOrList[A] = Either[List[String], A]

val errStr1: ErrorOrList[Int] = Left(List("error 1"))

val errStr2: ErrorOrList[Int] = Left(List("error 2"))

(errStr1, errStr2).parTupled

// res3: ErrorOrList[(Int, Int)] = Left(List("error 1", "error 2"))


Hay muchos métodos de sintaxis proporcionados por Parallel para métodos en Semigroupal y tipos relacionados, pero el más utilizado es parMapN.

Aquí hay un ejemplo de parMapN en una situación de manejo de errores.


val success1: ErrorOr[Int] = Right(1)

val success2: ErrorOr[Int] = Right(2)

val addTwo = (x: Int, y: Int) => x + y

(error1, error2).parMapN(addTwo)

// res4: ErrorOr[Int] = Left(Vector("Error 1", "Error 2"))

(success1, success2).parMapN(addTwo)

// res5: ErrorOr[Int] = Right(3)


Profundicemos en cómo funciona Parallel. La siguiente definición es el núcleo de Parallel.


trait Parallel[M[_]] {

type F[_]

def applicative: Applicative[F]

def monad: Monad[M]

def parallel: ~>[M, F]

}


Esto nos dice si hay una instancia paralela para algún constructor de tipo M, entonces:

• debe haber una instancia de Monad para M;

• hay un constructor de tipo relacionado F que tiene una instancia Aplicativa; y

• podemos convertir M a F.

No hemos visto ~> antes. Es un alias de tipo para FunctionK y es lo que realiza la conversión de M a F. Una función normal A => B convierte valores de tipo A a valores de tipo B. Recordemos que M y F no son tipos; son constructores de tipos. Una FunciónK M ~> F es una función de un valor con tipo M[A] a un valor con tipo F[A]. Veamos un ejemplo rápido definiendo una FunciónK que convierte un Option en una Lista.

import cats.arrow.FunctionK

object optionToList extends FunctionK[Option, List] {

def apply[A](fa: Option[A]): List[A] =

    fa match {

        case None => List.empty[A]

        case Some(a) => List(a)

    }

}

optionToList(Some(1))

// res6: List[Int] = List(1)

optionToList(None)

// res7: List[Nothing] = List()


Como el parámetro de tipo A es genérico, una función K no puede inspeccionar ningún valor contenido con el constructor de tipo M. La conversión debe realizarse únicamente en términos de la estructura de los constructores de tipo M y F. Podemos en optionToList arriba, este es el caso.

Entonces, en resumen, Parallel nos permite tomar un tipo que tiene una instancia de mónada y convertirlo en algún tipo relacionado que en su lugar tenga una instancia aplicativa (o semigrupal). Este tipo relacionado tendrá algunas semánticas alternativas útiles.

Hemos visto el caso anterior donde el aplicativo relacionado para "O" permite la acumulación de errores en lugar de una semántica rápida.

Ahora que hemos visto Parallel, es hora de aprender finalmente sobre Applicative.

jueves, 1 de diciembre de 2022

Semigroupal, Parallel y Applicative parte 5

La razón de los resultados sorprendentes de List y de Either es que ambas son mónadas. Si tenemos una mónada, podemos implementar el producto de la siguiente manera.


import cats.Monad

import cats.syntax.functor._ // for map

import cats.syntax.flatMap._ // for flatmap

def product[F[_]: Monad, A, B](fa: F[A], fb: F[B]): F[(A,B)] = 

fa.flatMap(a =>

    fb.map(b =>

        (a, b)

    ) 


Sería muy extraño si tuviéramos diferentes semánticas para el producto dependiendo de cómo lo implementemos. Para garantizar una semántica coherente, Cats' Monad (que amplía Semigroupal) proporciona una definición estándar de producto en términos de map y flatmap, como mostramos anteriormente.

Incluso nuestros resultados para Future son un truco de la luz. flatMap proporciona un orden secuencial, por lo que el producto proporciona lo mismo. La ejecución paralela que observamos ocurre porque nuestros futuros constituyentes comienzan a correr antes de que llamemos al producto. Esto es equivalente al patrón clásico create-then-flatMap:


val a = Future("Future 1")

val b = Future("Future 2")

for {

    x <- a

    y <- b

} yield (x, y)


Entonces, ¿por qué molestarse con Semigroupal? La respuesta es que podemos crear tipos de datos útiles que tengan instancias de Semigroupal (y Applicative) pero no Monad. Esto nos libera para implementar el producto de diferentes maneras. 

Semigroupal, Parallel y Applicative parte 4

Semigroupal no siempre proporciona el comportamiento que esperamos, particularmente para los tipos que también tienen instancias de Monad. Hemos visto el comportamiento del Semigroupal para Option. Veamos algunos ejemplos para otros tipos.

La semántica de Future proporciona ejecución paralela en lugar de secuencial:


import cats.Semigroupal

import cats.instances.future._ // for Semigroupal

import scala.concurrent._

import scala.concurrent.duration._

import scala.concurrent.ExecutionContext.Implicits.global

val futurePair = Semigroupal[Future].

product(Future("Hello"), Future(123))

Await.result(futurePair, 1.second)

// res0: (String, Int) = ("Hello", 123)


Los dos futuros comienzan a ejecutarse en el momento en que los creamos, por lo que ya están calculando los resultados en el momento en que llamamos producto. Podemos usar la sintaxis de aplicación para comprimir números fijos de futuros:


import cats.syntax.apply._ // for mapN

case class Cat(

    name: String,

    yearOfBirth: Int,

    favoriteFoods: List[String]

)

val futureCat = (

    Future("Garfield"),

    Future(1978),

    Future(List("Lasagne"))

).mapN(Cat.apply)

Await.result(futureCat, 1.second)

// res1: Cat = Cat("Garfield", 1978, List("Lasagne"))


La combinación de Listas con Semigroupal produce algunos resultados potencialmente inesperados. Podríamos esperar un código como el siguiente para comprimir las listas, pero en realidad obtenemos el producto cartesiano de sus elementos:


import cats.Semigroupal

import cats.instances.list._ // for Semigroupal

Semigroupal[List].product(List(1, 2), List(3, 4))

// res2: List[(Int, Int)] = List((1, 3), (1, 4), (2, 3), (2, 4))


Esto es quizás sorprendente. Comprimir listas tiende a ser una operación más común. 

Podríamos esperar que el producto aplicado a O acumule errores en lugar de fallar rápidamente. Nuevamente, tal vez sorprendentemente, encontramos que el producto implementa el mismo comportamiento de falla rápida que flatMap:

import cats.instances.either._ // for Semigroupal

type ErrorOr[A] = Either[Vector[String], A]

Semigroupal[ErrorOr].product(

Left(Vector("Error 1")),

Left(Vector("Error 2"))

)

// res3: ErrorOr[Tuple2[Nothing, Nothing]] = Left(Vector("Error 1"))

En este ejemplo, el producto ve la primera falla y se detiene, aunque es posible examinar el segundo parámetro y ver que también es una falla.




miércoles, 30 de noviembre de 2022

Semigroupal, Parallel y Applicative parte 3

Cats proporciona una sintaxis de aplicación simplificada. Importamos cats.syntax.apply.


import cats.instances.option._ // for Semigroupal

import cats.syntax.apply._ // for tupled and mapN


El método tuplado se agrega implícitamente a la tupla de Opciones. Utiliza el Semigroupal para Option para comprimir los valores dentro de las Opciones, creando una sola Opción de una tupla:


(Option(123), Option("abc")).tupled

// res8: Option[(Int, String)] = Some((123, "abc"))


Podemos usar el mismo truco en tuplas de hasta 22 valores. Cats define un método tuplado separado para cada aridad:


(Option(123), Option("abc"), Option(true)).tupled

// res9: Option[(Int, String, Boolean)] = Some((123, "abc", true))


Además de tuplado, la sintaxis de aplicación de Cats proporciona un método llamado mapN que acepta un Funtor implícito y una función de la aridad correcta para combinar los valores.


final case class Cat(name: String, born: Int, color: String)

(

Option("Garfield"),

Option(1978),

Option("Orange & black")

).mapN(Cat.apply)

// res10: Option[Cat] = Some(Cat("Garfield", 1978, "Orange & black"))


De todos los métodos mencionados aquí, el más común es usar mapN. Internamente, mapN usa Semigroupal para extraer los valores de Option y Functor para aplicar los valores a la función.

Es bueno ver que esta sintaxis está marcada. Si proporcionamos una función que acepta el número o tipo de parámetros incorrectos, obtenemos un error de compilación:


val add: (Int, Int) => Int = (a, b) => a + b

// add: (Int, Int) => Int = <function2>

(Option(1), Option(2), Option(3)).mapN(add)

// error: ':' expected but '(' found.

// Option("Garfield"),

// ^

(Option("cats"), Option(true)).mapN(add)

/ error: ':' expected but '(' found.

// Option("Garfield"),

// ^


Apply  también tiene métodos contramapN e imapN que aceptan funtores contravariantes e invariantes. Por ejemplo, podemos combinar Monoids usando Invariant. Aquí hay un ejemplo:

import cats.Monoid

import cats.instances.int._// for Monoid

import cats.instances.invariant._// for Semigroupal

import cats.instances.list._// for Monoid

import cats.instances.string._// for Monoid

import cats.syntax.apply._// for imapN


final case class Cat(

   name: String,

   yearOfBirth: Int,

   favoriteFoods: List[String]

)


val tupleToCat: (String, Int, List[String]) => Cat = Cat.apply _

val catToTuple: Cat => (String, Int, List[String]) = cat => (cat.name, cat.yearOfBirth, cat.favoriteFoods)

implicit val catMonoid: Monoid[Cat] = (

    Monoid[String],

    Monoid[Int],

    Monoid[List[String]]

).imapN(tupleToCat)(catToTuple)


Nuestro Monoid nos permite crear Cats "vacíos" y agregar Cats usando la sintaxis :


import cats.syntax.semigroup._ // for |+|

val garfield= Cat("Garfield", 1978, List("Lasagne"))

val heathcliff = Cat("Heathcliff", 1988, List("Junk Food"))

garfield |+| heathcliff

// res14: Cat = Cat("GarfieldHeathcliff", 3966, List("Lasagne", "Junk

Food"))


domingo, 27 de noviembre de 2022

Semigroupal, Parallel y Applicative parte 2

cats.Semigroupal es una clase de tipos que nos permite combinar contextos. Si tenemos dos objetos de tipo F[A] y F[B], un Semigroupal[F] nos permite combinarlos para formar un F[(A, B)]. Su definición en Cats es:


trait Semigroupal[F[_]] {

    def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]

}


Como comentamos en un post anterior, los parámetros fa y fb son independientes entre sí: podemos calcularlos en cualquier orden antes de pasarlos al producto. Esto contrasta con flatMap, que impone un orden estricto en sus parámetros. Esto nos da más libertad cuando definimos instancias de Semigroupal que cuando definimos Monads. 

Mientras que Semigroup nos permite unir valores, Semigroupal nos permite unir contextos. Juntemos algunas Opciones como ejemplo:

import cats.Semigroupal

import cats.instances.option._ // for Semigroupal

Semigroupal[Option].product(Some(123), Some("abc"))

// res1: Option[(Int, String)] = Some((123, "abc"))


Si ambos parámetros son instancias de Some, terminamos con una tupla de los valores dentro. Si alguno de los parámetros se evalúa como None, el resultado completo es None:


Semigroupal[Option].product(None, Some("abc"))

// res2: Option[Tuple2[Nothing, String]] = None

Semigroupal[Option].product(Some(123), None)

// res3: Option[Tuple2[Int, Nothing]] = None


El objeto complementario de Semigroupal define un conjunto de métodos además del producto. Por ejemplo, los métodos tuple2 a tuple22 generalizan el producto a diferentes aridades:


import cats.instances.option._ // for Semigroupal

Semigroupal.tuple3(Option(1), Option(2), Option(3))

// res4: Option[(Int, Int, Int)] = Some((1, 2, 3))

Semigroupal.tuple3(Option(1), Option(2), Option.empty[Int])

// res5: Option[(Int, Int, Int)] = None


Los métodos map2 a map22 aplican una función especificada por el usuario a los valores dentro de 2 a 22 contextos:


Semigroupal.map3(Option(1), Option(2), Option(3))(_ + _ + _)

// res6: Option[Int] = Some(6)

Semigroupal.map2(Option(1), Option.empty[Int])(_ + _)

// res7: Option[Int] = None


También existen métodos contramap2 a contramap22 e imap2 a imap22, que requieren instancias de Contravariant e Invariant respectivamente.

Solo hay una ley para Semigroupal: el método del producto debe ser asociativo.


product(a, product(b, c)) == product(product(a, b), c)



miércoles, 23 de noviembre de 2022

Semigroupal, Parallel y Applicative


Los funtores y las mónadas nos permiten secuenciar operaciones usando map y flatMap. Si bien los funtores y las mónadas son abstracciones inmensamente útiles, hay ciertos tipos de flujo de programa que no pueden representar.

Un ejemplo de ello es la validación de formularios. Cuando validamos un formulario, queremos devolver todos los errores al usuario, no detenernos en el primer error que encontremos. Si modelamos esto con una mónada como Either, fallamos rápidamente y perdemos errores. Por ejemplo, el siguiente código falla en la primera llamada a parseInt y no avanza más:


import cats.syntax.either._ // for catchOnly


def parseInt(str: String): Either[String, Int] = Either.catchOnly[NumberFormatException](str.toInt).

leftMap(_ => s"Couldn't read $str")

for {

    a <- parseInt("a")

    b <- parseInt("b")

    c <- parseInt("c")

} yield (a + b + c)

// res0: Either[String, Int] = Left("Couldn't read a")


Otro ejemplo es la evaluación concurrente de Futures. Si tenemos varias tareas independientes de larga duración, tiene sentido ejecutarlas simultáneamente. Sin embargo, la comprensión monádica solo nos permite ejecutarlos en secuencia. map y flatMap no son del todo capaces de capturar lo que queremos porque suponen que cada cálculo depende del anterior:


// context2 is dependent on value1:

context1.flatMap(value1 => context2)


Las llamadas a parseInt y Future.apply anteriores son independientes entre sí, pero map y flatMap no pueden aprovechar esto. Necesitamos una construcción más débil, una que no garantice la secuenciación, para lograr el resultado que queremos. Veremos tres clases de tipos que soportan este patrón:

  • Semigroupal abarca la noción de componer pares de contextos. Cats proporciona un módulo cats.syntax.apply que utiliza Semigroupal y Functor para permitir a los usuarios secuenciar funciones con múltiples argumentos.
  • Parallel convierte tipos con una instancia de Monad en un tipo relacionado con una instancia de Semigroupal.
  • Applicative extiende Semigroupal y Functor. Proporciona una forma de aplicar funciones a parámetros dentro de un contexto.E

Applicatives a menudo se formulan en términos de aplicación de funciones, en lugar de la formulación semigrupal que se enfatiza en Cats. Esta formulación alternativa proporciona un enlace a otras bibliotecas y lenguajes como Scalaz y Haskell. Veremos las diferentes formulaciones de Applicative, así como las relaciones entre Semigroupal, Functor, Applicative y Monad.

viernes, 21 de octubre de 2022

Transformadores de mónadas en cats parte 5


El uso generalizado de transformadores de mónadas a veces es difícil porque fusionan las mónadas de formas predefinidas. Sin una reflexión cuidadosa, podemos terminar teniendo que desempaquetar y volver a empaquetar mónadas en diferentes configuraciones para operar con ellas en diferentes contextos.

Podemos hacer frente a esto de múltiples maneras. Un enfoque consiste en crear una sola "súper pila" y adherirse a ella en toda nuestra base de código. Esto funciona si el código es simple y en gran medida de naturaleza uniforme. Por ejemplo, en una aplicación web, podríamos decidir que todos los controladores de solicitudes son asíncronos y todos pueden fallar con el mismo conjunto de códigos de error HTTP. Podríamos diseñar un ADT personalizado que represente los errores y usar una fusión Future y Either en todas partes de nuestro código:

sealed abstract class HttpError

final case class NotFound(item: String) extends HttpError

final case class BadRequest(msg: String) extends HttpError

// etc...

type FutureEither[A] = EitherT[Future, HttpError, A]


El enfoque de "súper pila" comienza a fallar en bases de código más grandes y heterogéneas donde las diferentes pilas tienen sentido en diferentes contextos. Otro patrón de diseño que tiene más sentido en estos contextos utiliza transformadores de mónadas como "código de unión" local. Exponemos las pilas no transformadas en los límites del módulo, las transformamos para operarlas localmente y las destransformamos antes de pasarlas. Esto permite que cada módulo de código tome sus propias decisiones sobre qué transformadores usar:


import cats.data.Writer

type Logged[A] = Writer[List[String], A]

// Methods generally return untransformed stacks:

def parseNumber(str: String): Logged[Option[Int]] = util.Try(str.toInt).toOption match {

     case Some(num) => Writer(List(s"Read $str"), Some(num))

     case None => Writer(List(s"Failed on $str"), None)

}

// Consumers use monad transformers locally to simplify composition:

def addAll(a: String, b: String, c: String): Logged[Option[Int]] = {

    import cats.data.OptionT

    val result = for {

        a <- OptionT(parseNumber(a))

        b <- OptionT(parseNumber(b))

        c <- OptionT(parseNumber(c))

    } yield a + b + c

    result.value

}

// This approach doesn't force OptionT on other users' code:

val result1 = addAll("1", "2", "3")

// result1: Logged[Option[Int]] = WriterT(

//    (List("Read 1", "Read 2", "Read 3"), Some(6))

// )

val result2 = addAll("1", "a", "3")

// result2: Logged[Option[Int]] = WriterT(

//    (List("Read 1", "Failed on a"), None)

// )


Desafortunadamente, no existen enfoques únicos para trabajar con transformadores de mónadas. El mejor enfoque para usted puede depender de muchos factores: el tamaño y la experiencia de su equipo, la complejidad de su código base, etc. Es posible que deba experimentar y recopilar comentarios de colegas para determinar si los transformadores de mónadas son una buena opción.