使用 monad 转换器更改表达式结果
Using monad transformers change expression result
我发现 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 的便捷方式,但这似乎增加了更多深度,因为无论我是否使用它们,它实际上可能会产生不同的值。
让我指出导致这种行为的因素:
- monad transformer 提供了一种数据类型,它“专注于”内部的 monad,以一种允许程序员忽略与“外部”数据类型相关的 handling/plumbing 的方式,但结合了两者的功能
.tupled
只是链式 .ap
/.zip
调用的语法糖,它又必须与 .flatMap
一致
然后,如果我们将其写成 flatMap
s:
的序列,那么在特定情况下发生的事情会变得更加明显
(success, empty, failed).tupled.value // Right(None)
- empty
对整个堆栈进行短路评估(使用 OptionT
的重点!),因此 failed
不是 executed/taken考虑
(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 转换器的行为对我来说一点也不直观。
以下数据为例:
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 的便捷方式,但这似乎增加了更多深度,因为无论我是否使用它们,它实际上可能会产生不同的值。
让我指出导致这种行为的因素:
- monad transformer 提供了一种数据类型,它“专注于”内部的 monad,以一种允许程序员忽略与“外部”数据类型相关的 handling/plumbing 的方式,但结合了两者的功能
.tupled
只是链式.ap
/.zip
调用的语法糖,它又必须与.flatMap
一致
然后,如果我们将其写成 flatMap
s:
(success, empty, failed).tupled.value // Right(None)
-empty
对整个堆栈进行短路评估(使用OptionT
的重点!),因此failed
不是 executed/taken考虑(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))