堆叠 M、Either 和 Writer

5

我目前正在使用EitherT堆叠Futures和Eithers:

type ErrorOr[A] = Either[Error, A]

def getAge: Future[ErrorOr[Int]] = ???
def getDob(age: Int): ErrorOr[LocalDate] = ???

for {
  age <- EitherT(getAge)
  dob <- EitherT.fromEither[Future](getDob(age))
} yield dob

我现在想介绍一下Writer Monad,即

type MyWriter[A] = Writer[Vector[String], ErrorOr[A]]

def getAge: Future[MyWriter[Int]] = ???
def getDob(age: Int): MyWriter[LocalDate] = ???

我的问题是,如何最好地对getAgegetDob进行排序调用?我知道可以堆叠单子,即Future -> Writer -> Either,但在这种情况下,我能否继续使用EitherT?如果可以,应该如何使用?

2个回答

7
这是对@luka-jacobowitz提供的方法的轻微改进。在他的方法中,任何发生在“失败”之前的日志都会丢失。根据建议的类型:
type FutureErrorOr[A] = EitherT[Future, Error, A]
type MyStack[A] = WriterT[FutureErrorOr, Vector[String], A]

我们发现,如果我们使用WriterTrun方法来扩展MyStack[A]的值,我们将得到以下类型的值:
FutureErrorOr[(Vector[String], A)]

这与以下内容相同:

EitherT[Future, Error, (Vector[String], A)]

我们可以用EitherTvalue方法进一步扩展:

Future[Either[Error, (Vector[String], A)]]

在这里,我们可以看到,仅当程序“成功”(即右结合)时,才能检索包含结果日志的元组。如果程序失败,则无法访问程序运行时创建的任何先前日志。如果我们采用原始示例并稍微修改一下,在每个步骤之后记录一些内容,假设第二步返回类型为Left [Error]的值:
val program = for {
  age <- WriterT.liftF(getAge)
  _ <- WriterT.tell(Vector("Got age!"))
  dob <- WriterT.liftF(EitherT.fromEither(getDob(age))) // getDob returns Left[Error]
  _ <- WriterT.tell(Vector("Got date of birth!"))
} yield {
  dob
}

那么当我们评估结果时,只会返回包含错误的左侧情况而没有任何日志。
val expanded = program.run.value // Future(Success(Left(Error)))
val result = Await.result(expanded, Duration.apply(2, TimeUnit.SECONDS)) // Left(Error), no logs!!

为了获取程序运行结果和在程序失败之前生成的日志,我们可以按照以下方式重新排列建议的单子:
type MyWriter[A] = WriterT[Future, Vector[String], A]
type MyStack[A] = EitherT[MyWriter, Error, A]

现在,如果我们使用EitherTvalue方法扩展MyStack[A],我们将得到以下类型的值:
WriterT[Future, Vector[String], Either[Error, A]]

我们可以使用 WriterTrun 方法进一步扩展,得到一个包含日志和结果值的元组:
Future[(Vector[String], Either[Error, A])]

采用这种方法,我们可以将程序重写为:

val program = for {
  age <- EitherT(WriterT.liftF(getAge.value))
  _ <- EitherT.liftF(WriterT.put(())(Vector("Got age!")))
  dob <- EitherT.fromEither(getDob(age))
  _ <- EitherT.liftF(WriterT.put(())(Vector("Got date of birth!")))
} yield {
  dob
}

当我们运行程序时,即使在执行过程中出现故障,我们也可以访问生成的日志:

val expanded = program.value.run // Future(Success((Vector("Got age!), Left(Error))))
val result = Await.result(expanded, Duration.apply(2, TimeUnit.SECONDS)) // (Vector("Got age!), Left(Error))

不可否认,这种解决方案需要编写更多的样板代码,但我们始终可以定义一些帮助程序来辅助完成:

implicit class EitherTOps[A](eitherT: FutureErrorOr[A]) {
  def lift: EitherT[MyWriter, Error, A] = {
    EitherT[MyWriter, Error, A](WriterT.liftF[Future, Vector[String], ErrorOr[A]](eitherT.value))
  }
}

implicit class EitherOps[A](either: ErrorOr[A]) {
  def lift: EitherT[MyWriter, Error, A] = {
    EitherT.fromEither[MyWriter](either)
  }
}

def log(msg: String): EitherT[MyWriter, Error, Unit] = {
  EitherT.liftF[MyWriter, Error, Unit](WriterT.put[Future, Vector[String], Unit](())(Vector(msg)))
}

val program = for {
  age <- getAge.lift
  _ <- log("Got age!")
  dob <- getDob(age).lift
  _ <- log("Got date of birth!")
} yield {
  dob
}

1
不确定为什么这条评论没有被点赞。非常有用的见解,谢谢Luis。 - Observer

7
是的,你可以像这样继续使用WriterT单子变换器:
type FutureErrorOr[A] = EitherT[Future, Error, A]
type MyStack[A] = WriterT[FutureErrorOr, Vector[String], A]

如果你拆解这个类型,它类似于 `Future[Either[Error, Writer[Vector[String], A]]`。
现在棘手的部分是将您的函数提升到此基本单子中,以下是一些示例:
def getAge: FutureErrorOr[Int] = ???
def getDob(age: Int): ErrorOr[LocalDate] = ???

for {
  age <- WriterT.liftF(getAge)
  dob <- WriterT.liftF(EitherT.fromEither(getDob(age)))
} yield dob

为了让这更容易,您可以查看 cats-mtl。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接