viernes, 7 de octubre de 2022

Definir monadas con Cats

Podemos definir una Monad para un tipo personalizado proporcionando implementaciones de tres métodos: flatMap, pure y un método llamado tailRecM. Aquí hay una implementación de Monad para Option como ejemplo:


import cats.Monad

import scala.annotation.tailrec


val optionMonad = new Monad[Option] {

def flatMap[A, B](opt: Option[A]) (fn: A => Option[B]): Option[B] = opt flatMap fn

def pure[A](opt: A): Option[A] = Some(opt)


@tailrec

def tailRecM[A, B](a: A) (fn: A => Option[Either[A, B]]): Option[B] =

     fn(a) match {

          case None => None

          case Some(Left(a1)) => tailRecM(a1)(fn)

          case Some(Right(b)) => Some(b)

     }

}

El método tailRecM es una optimización utilizada en Cats para limitar la cantidad de espacio de pila que consumen las llamadas anidadas a flatMap. viene la técnica de un artículo de 2015 del creador de PureScript, Phil Freeman. El método se llama recursivamente a sí mismo hasta que el resultado de fn devuelve un Right.

Supongamos que queremos escribir un método que llama a una función hasta que la función indica que debe detenerse. los devolverá una instancia de mónada porque, como sabemos, las mónadas representan secuenciación y muchas mónadas tienen alguna noción de fin de ejecución.

Podemos escribir este método en términos de flatMap.

import cats.syntax.flatMap._ // For flatMap

def retry[F[_]: Monad, A](start: A)(f: A => F[A]): F[A] =

     f(start).flatMap{ a =>

          retry(a)(f)

}


Desafortunadamente, no es seguro. Funciona para entradas pequeñas.


import cats.instances.option._

retry(100)(a => if(a == 0) None else Some(a - 1))

// res1: Option[Int] = None


pero si intentamos una entrada grande, obtenemos un StackOverflowError.


retry(100000)(a => if(a == 0) None else Some(a - 1))


En su lugar, podemos reescribir este método usando tailRecM.


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

def retryTailRecM[F[_]: Monad, A](start: A)(f: A => F[A]): F[A] =

Monad[F].tailRecM(start){ a =>

     f(a).map(a2 => Left(a2))

}


Ahora se ejecuta con éxito sin importar cuántas veces recurramos.


retryTailRecM(100000)(a => if(a == 0) None else Some(a - 1))

// res2: Option[Int] = None


Es importante tener en cuenta que tenemos que llamar explícitamente a tailRecM. No hay una transformación de código que convierta el código recursivo sin cola en código recursivo de cola que use tailRecM. Sin embargo, la clase de tipos Monad proporciona varias utilidades que facilitan la escritura de este tipo de métodos.

Por ejemplo, podemos reescribir retry en términos de iterateWhileM y no tenemos que llamar explícitamente a tailRecM.


import cats.syntax.monad._ // for iterateWhileM

def retryM[F[_]: Monad, A](start: A)(f: A => F[A]): F[A] = start.iterateWhileM(f)(a => true)

retryM(100000)(a => if(a == 0) None else Some(a - 1))

// res3: Option[Int] = None


Todas las mónadas incorporadas en Cats tienen implementaciones recursivas de cola de tailRecM, aunque escribir una para mónadas personalizadas puede ser un desafío... 

No hay comentarios.:

Publicar un comentario