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.