如何在for表达式中使用scalaz.WriterT进行日志记录?

22

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

2个回答

34

关于Monad变换器

这是一个非常简短的介绍。你可以在haskellwiki或者这个@jrwest的幻灯片中找到更多信息。

Monad 不能组合,也就是说如果你有一个 Monad A[_] 和一个 Monad B[_],那么自动推导出 Monad A[B[_]] 是不可能的。然而,在大多数情况下,通过为给定的 Monad 提供所谓的 Monad 变换器可以实现此目标。

如果我们有 Monad 变换器 BT 用于 Monad B,那么我们可以组合一个新的 Monad A[B[_]] 用于任何 Monad A。没错,通过使用 BT,我们可以将 B 放入 A 中。

Scalaz 中的 Monad 变换器用法

以下假设使用 scalaz 7,因为老实说我没有在 scalaz 6 中使用过 Monad 变换器

Monad 变换器 MT 接受两个类型参数,第一个是包装(外部)Monad,第二个是 Monad 堆栈底部的实际数据类型。注意:它可能需要更多的类型参数,但这些与变换器特性无关,而是针对该给定 Monad 的特定参数(例如 Writer 的记录类型或Validation 的错误类型)。

因此,如果我们有 List[Option[A]] 想将其视为单个组合的 Monad,则需要 OptionT[List, A]。如果我们有 Option[List[A]],则需要 ListT [Option,A]

如何实现?如果我们具有非变换器值,通常可以使用 MT.apply 将其包装以获取变换器内部的值。要将转换后的值返回到正常状态,通常在转换后的值上调用 .run

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

Tony Morris 建议尽早进入变换版本并尽可能长时间地使用它。

注意:使用变换器组合多个 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 进行日志记录

根据您的需要,您可以在没有特定外部 Monad 的情况下使用 WriterT(在这种情况下,它将使用不执行任何操作的Id Monad),也可以将日志记录放在一个 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需要日志类型是一个semigroup才能组合日志值。

第二个案例,单子内部记录日志

我们选择Option单子来简化问题。

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))

使用这种方法,由于日志记录在Option单子内部,如果任何一个绑定的选项是None,我们将只会得到None结果,不会有任何日志。

注意:x.point[Option]的效果与Some(x)相同,但可能有助于更好地推广代码。暂时只是这样做而已,没有致命问题。

第三个选择,日志记录在单子之外

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))等等。

在这些示例中有许多明确且冗长的代码(类型、调用)。与往常一样,这些是为了清晰明了,可以通过提取公共类型和操作来在您的代码中进行重构。


注意:在scalaz-seven head中即将推出MonadWriter类型类。值得关注。 - ron

0
type OptionLogger[A] = WriterT[Option, NonEmptyList[String], A]

      val two: OptionLogger[Int] = WriterT.put(2.some)("The number two".pure[NonEmptyList])
      val hundred: OptionLogger[Int] = WriterT.put(100.some)("One hundred".pure[NonEmptyList])

      val twoHundred = for {
        a <- two
        b <- hundred
      } yield a * b

      twoHundred.value must be equalTo(200.some)


      val log = twoHundred.written map { _.list } getOrElse List() mkString(" ")
      log must be equalTo("The number two One hundred")

1
我记得看到这个Gist(由@puffnfresh推特)https://gist.github.com/3345722 - ron

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