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...