22

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

4

2 回答 2

34

关于单子变压器

这是一个非常简短的介绍。您可以在haskellwiki或@jrwest 的这张精彩幻灯片上找到更多信息。

Monads 不组合,这意味着如果你有一个 monadA[_]和一个 monad B[_],那么A[B[_]]就不能自动派生。然而,在大多数情况下,这可以通过为给定的 monad 使用所谓的 monad 转换器来实现。

如果我们有 monad 的 monad 转换BTB,那么我们可以A[B[_]]任何 monad 组成一个新的 monad A。没错,通过使用BT,我们可以放入B里面A

scalaz中的Monad变压器使用

以下假设为 scalaz 7,因为坦率地说,我没有将 monad 转换器与 scalaz 6 一起使用

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

因此,如果我们List[Option[A]]想要将其视为单个组合的 monad,那么我们需要OptionT[List, A]. 如果我们有Option[List[A]],我们需要ListT[Option, A]

到那里怎么走?如果我们有 non-transformer 值,我们通常可以将其包装起来MT.apply以获取 Transformer 内部的值。为了从转换后的形式恢复正常,我们通常调用.run转换后的值。

所以val a: OptionT[List, Int] = OptionT[List, Int](List(some(1))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没有任何特定外部 monad(在这种情况下,在后台它将使用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的,因为需要日志类型为半组才能组合日志值。

第二种情况,在 monad 中登录

在这里,为了简单起见,我们选择了Optionmonad。

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没有任何日志的结果。

注意: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)

在这里,我们使用OptionTOptionmonad 放在Writer. 其中一项计算None表明,即使在这种情况下,日志也会被保留。

最后的评论

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

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

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

于 2012-08-14T11:59:33.827 回答
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")
于 2012-08-15T00:21:58.790 回答