Por convención, en Cats una mónada Foo tendrá una clase transformadora llamada FooT. De hecho, muchas mónadas en Cats se definen combinando un transformador de mónadas con la mónada Id. Concretamente, algunas de las instancias disponibles son:
- cats.data.OptionT for Option;
- cats.data.EitherT for Either;
- cats.data.ReaderT for Reader;
- cats.data.WriterT for Writer;
- cats.data.StateT for State;
- cats.data.IdT for the Id monad.
Todos estos transformadores de mónadas siguen la misma convención. El transformador en sí representa la mónada interna en una pila, mientras que el primer parámetro de tipo especifica la mónada externa. Los parámetros de tipo restantes son los tipos que hemos usado para formar las mónadas correspondientes.
Por ejemplo, nuestro tipo ListOption del ejemplo anterior es un alias para OptionT[List, A] pero el resultado es efectivamente una List[Option[A]]. En otras palabras, construimos pilas de mónadas de adentro hacia afuera:
type ListOption[A] = OptionT[List, A]
Muchas mónadas y todos los transformadores tienen al menos dos parámetros de tipo, por lo que a menudo tenemos que definir alias de tipo para etapas intermedias. Por ejemplo, supongamos que queremos envolver Either alrededor de Option. Option es el tipo más interno, por lo que queremos usar el transformador de mónada OptionT. Necesitamos usar Either como el primer parámetro de tipo. Sin embargo, Either dos parámetros de tipo y las mónadas solo tienen uno. Necesitamos un alias de tipo para convertir el constructor de tipo a la forma correcta:
// Alias Either to a type constructor with one parameter:
type ErrorOr[A] = Either[String, A]
// Build our final monad stack using OptionT:
type ErrorOrOption[A] = OptionT[ErrorOr, A]
ErrorOrOption es una mónada, al igual que ListOption. Podemos usar pure, map y flatMap como de costumbre para crear y transformar instancias:
import cats.instances.either._ // for Monad
val a = 10.pure[ErrorOrOption]
// a: ErrorOrOption[Int] = OptionT(Right(Some(10)))
val b = 32.pure[ErrorOrOption]
// b: ErrorOrOption[Int] = OptionT(Right(Some(32)))
val c = a.flatMap(x => b.map(y => x + y))
// c: OptionT[ErrorOr, Int] = OptionT(Right(Some(42)))
Las cosas se vuelven aún más confusas cuando queremos apilar tres o más mónadas. Por ejemplo, vamos a crear un Future de Either de Option. Una vez más, construimos esto de adentro hacia afuera con una OptionT de una EitherT de Future. Sin embargo, no podemos definir esto en una sola línea porque EitherT tiene tres parámetros de tipo:
case class EitherT[F[_], E, A](stack: F[Either[E, A]]) {
// etc...
}
Los tres parámetros de tipo son los siguientes:
- F[_] es la mónada externa en la pila (Either es la interna);
- E es el tipo de error para el Either;
- A es el tipo de resultado para Either.
Esta vez creamos un alias para EitherT que corrige Future y Error y permite que A varíe:
import scala.concurrent.Future
import cats.data.{EitherT, OptionT}
type FutureEither[A] = EitherT[Future, String, A]
type FutureEitherOption[A] = OptionT[FutureEither, A]
Nuestra gigantesca pila ahora compone tres mónadas y nuestros métodos map y flatMap atraviesan tres capas de abstracción:
import cats.instances.future._ // for Monad
import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
val futureEitherOr: FutureEitherOption[Int] =
for {
a <- 10.pure[FutureEitherOption]
b <- 32.pure[FutureEitherOption]
} yield a + b