如何使用 scalaz.WriterT 记录 for 表达式?

2023-12-21

如何使用 scalaz.WriterT 进行日志记录?


关于 Monad 变压器

这是一个非常简短的介绍。您可能会找到更多信息哈斯克尔维基 http://www.haskell.org/haskellwiki/Monad_Transformers_Explained或这个@jrwest 制作的很棒的幻灯片 https://speakerdeck.com/u/jrwest/p/monad-transformers.

Monad 不会组合,这意味着如果你有一个 monadA[_]和一个单子B[_], then A[B[_]]无法推导自动地。然而,在大多数情况下,这可以通过针对给定单子使用所谓的单子变压器来实现。

如果我们有 monad 变压器BT对于单子B,然后我们可以组成一个新的 monadA[B[_]] for 任何单子 A。没错,通过使用BT,我们可以把B inside A.

Monad 变压器在 scalaz 中的使用

以下假设 scalaz 7,因为坦率地说,我没有在 scalaz 6 中使用 monad 转换器.

单子变压器MT采用两个类型参数,第一个是包装器(外部)monad,第二个是 monad 堆栈底部的实际数据类型。注意:它可能需要更多类型参数,但这些参数与转换器性质无关,而是特定于给定的 monad(例如 a 的记录类型)Writer,或a的错误类型Validation).

所以如果我们有一个List[Option[A]]我们想将其视为单个组合单子,那么我们需要OptionT[List, A]。如果我们有Option[List[A]], 我们需要ListT[Option, A].

到那里怎么走?如果我们有非转换器值,我们通常可以将其包装为MT.apply获取变压器内部的值。为了从转换后的形式恢复到正常形式,我们通常调用.run关于变换后的值。

So val a: OptionT[List, Int] = OptionT[List, Int](List(some(1)) and val b: List[Option[Int]] = a.run是相同的数据,只是表示方式不同。

托尼·莫里斯建议,最好尽早进入转换版本并尽可能长时间地使用它。

注意:使用变压器组合多个 monad 会产生一个变压器堆栈,其类型与普通数据类型的顺序正好相反。所以一个正常的List[Option[Validation[E, A]]]看起来像type ListOptionValidation[+E, +A] = ValidationT[({type l[+a] = OptionT[List, a]})#l, E, A]

更新:从 scalaz 7.0.0-M2 开始,Validation(正确地)不是 Monad,所以ValidationT不存在。使用EitherT反而。

使用 WriterT 进行日志记录

根据您的需要,您可以使用WriterT没有任何特定的外部单子(在这种情况下,在后台它将使用Idmonad 不做任何事情),或者可以将日志记录放入 monad 中,或者将 monad 放入日志记录中。

第一种情况,简单的日志记录

import scalaz.{Writer}
import scalaz.std.list.listMonoid
import scalaz._

def calc1 = Writer(List("doing calc"), 11)
def calc2 = Writer(List("doing other"), 22)

val r = for {
  a <- calc1
  b <- calc2
} yield {
  a + b
}

r.run should be_== (List("doing calc", "doing other"), 33)

我们进口listMonoid实例,因为它还提供了Semigroup[List]实例。因为需要它WriterT需要日志类型是半群以便能够组合日志值。

第二种情况,在 monad 内登录

这里我们选择了Option为简单起见,使用 monad。

import scalaz.{Writer, WriterT}
import scalaz.std.list.listMonoid
import scalaz.std.option.optionInstance
import scalaz.syntax.pointed._

def calc1 = WriterT((List("doing calc") -> 11).point[Option])
def calc2 = WriterT((List("doing other") -> 22).point[Option])

val r = for {
  a <- calc1
  b <- calc2
} yield {
  a + b
}

r.run should be_== (Some(List("doing calc", "doing other"), 33))

使用这种方法,由于日志记录位于Optionmonad,如果任何绑定选项是None,我们只会得到一个None结果没有任何日志。

Note: x.point[Option]效果与Some(x),但可能有助于更好地概括代码。不是致命的,只是暂时这样做了。

第三种选择,在 monad 之外记录

import scalaz.{Writer, OptionT}
import scalaz.std.list.listMonoid
import scalaz.std.option.optionInstance
import scalaz.syntax.pointed._

type Logger[+A] = WriterT[scalaz.Id.Id, List[String], A]

def calc1 = OptionT[Logger, Int](Writer(List("doing calc"), Some(11): Option[Int]))
def calc2 = OptionT[Logger, Int](Writer(List("doing other"), None: Option[Int]))

val r = for {
  a <- calc1
  b <- calc2
} yield {
  a + b
}

r.run.run should be_== (List("doing calc", "doing other") -> None)

这里我们使用OptionTOption里面的单子Writer。其中一项计算是None以表明即使在这种情况下日志也会被保留。

最后的评论

在这些例子中List[String]被用作日志类型。然而使用String这几乎不是最好的方法,只是日志框架强加给我们的一些约定。例如,最好定义一个自定义日志ADT,如果需要输出,请尽可能晚地将其转换为字符串。这样您就可以序列化日志的 ADT,并在以后以编程方式轻松分析它(而不是解析字符串)。

WriterT有许多有用的方法可以用来简化日志记录,请查看源代码。例如给定一个w: WriterT[...],您可以使用添加新的日志条目w :++> List("other event"),或者甚至使用当前持有的值进行记录w :++>> ((v) => List("the result is " + v)), etc.

示例中有许多显式且较长的代码(类型、调用)。与往常一样,这些都是为了清楚起见,通过提取常见类型和操作在代码中重构它们。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何使用 scalaz.WriterT 记录 for 表达式? 的相关文章

随机推荐