Translate

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.