使用 monad 转换器改变表达式结果

Posted

技术标签:

【中文标题】使用 monad 转换器改变表达式结果【英文标题】:Using monad transformers change expression result 【发布时间】:2021-12-11 06:00:21 【问题描述】:

我发现 monad 转换器的行为对我来说根本不直观。

以以下数据为例:

type F[X] = OptionT[Either[String, *], X]

val success: F[Int] = 1.pure[F]

val empty: F[Int] = ().raiseError[F, Int]

val failed = "Boom!".raiseError[Either[String, *], Int].liftTo[F]

然后执行一行:

(success, empty, failed).tupled.value // Right(None)

我们仍然得到Right,但我希望看到Left("Boom!"),因为Either 是最外层的效果。但是当订单稍作修改时:

(success, failed, empty).tupled.value // Left(Boom!)

这会产生一个预期值。另一件事是当我们在 tupled 之前从 monad 转换器中取出值并按初始顺序应用它们:

(success.value, empty.value, failed.value).tupled // Left(Boom!)

我们得到的值在我看来很直观,但与第一个示例的结果不一致。

有谁知道为什么 monad 转换器会以这种方式运行?我只是认为 monad 转换器是一种使用堆叠 monad 的便捷方式,但这似乎增加了更多深度,因为无论我是否使用它们,它实际上都可能产生不同的值。

【问题讨论】:

变形金刚用自己的行为形成了自己不同的Monads,与展开的版本不同。 OptionT 的意义在于,如果其中的值是 F[None],那么 flatMap 将返回另一个 F[None],就像 None 上的 flatMap 返回 None 一样。 【参考方案1】:

让我指出导致这种行为的因素:

monad 转换器提供了一种数据类型,即“关注”内部的 monad,在某种程度上允许程序员忽略与“外部”数据类型相关的处理/管道,但结合了两者的功能 .tupled 只是链式.ap/.zip 调用的语法糖,而这又必须与.flatMap 保持一致

然后,如果我们将其编写为flatMaps 的序列,则在特定情况下会发生什么变得更加明显:

(success, empty, failed).tupled.value // Right(None) - empty 对整个堆栈进行短路评估(使用 OptionT 的全部要点!),因此不执行/考虑 failed (success, failed, empty).tupled.value // Left(Boom!) - 这次是 failed,虽然在外部类型上会短路评估 (success.value, empty.value, failed.value).tupled // Left(Boom!) - 这里所有的值都是Either 值,所以是failed,表示“失败”

这种特殊行为——一种效果以某种方式“覆盖”或向另一种效果添加新语义,因为使用了转换器,通常需要小心处理,因为它显示了堆叠顺序变得多么重要——我了解到它是堆栈中Writer[T] 位置的示例,当用于记录时 - 它必须处于正确的位置,以免忘记在存在例如的情况下写入日志错误。

以下是此类行为的示例:

import cats._
import cats.data._
import cats.implicits._
import cats.mtl._
import cats.mtl.syntax.all._
import cats.effect.IO
import cats.effect.unsafe.implicits.global

def print[A: Show](value: A): IO[Unit] = IO  println(value.show) 

type Foo[A] = WriterT[EitherT[IO, String, _], List[String], A]

def runFoo[A: Show](value: Foo[A]): Unit = 
  value.run.value.flatMap(print).unsafeRunSync()


type Bar[A] = EitherT[WriterT[IO, List[String], _], String, A]

def runBar[A: Show](value: Bar[A]): Unit = 
  value.value.run.flatMap(print).unsafeRunSync()


def doSucceed[F[_]: Monad](
    value: Int
)(using t: Tell[F, List[String]]): F[Int] = 
  for 
    _ <- t.tell(s"Got value $value" :: Nil)
    newValue = value + 1
    _ <- t.tell(s"computed: $newValue" :: Nil)
   yield newValue


def doFail[F[_]](
    value: Int
)(using t: Tell[F, List[String]], err: MonadError[F, String]): F[Int] = 
  for 
    _ <- t.tell(s"Got value $value" :: Nil)
    _ <- "Boo".raiseError[F, Int]
   yield value


runFoo(doSucceed[Foo](42)) // prints Right((List(Got value 42, computed: 43),43))
runBar(doSucceed[Bar](42)) // prints (List(Got value 42, computed: 43),Right(43))

runFoo(doFail[Foo](42)) // prints Left(Boo)
runBar(doFail[Bar](42)) // prints (List(Got value 42),Left(Boo))

【讨论】:

以上是关于使用 monad 转换器改变表达式结果的主要内容,如果未能解决你的问题,请参考以下文章

`ap zip tail` 表达式是如何工作的

用于 IO monad 的复杂 monad 转换器

Java表达式转型规则

为什么布尔表达式中元素的顺序会改变结果? [重复]

在 SQLite 上转换表达式结果,从浮点数到十进制数

使用 Monad Transformers 避免孤儿实例