如何使用 scalaz.WriterT 进行日志记录?
关于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)
在这里我们使用OptionT
将Option
单子放入Writer
中。其中一个计算结果是None
,以展示即使在这种情况下,日志也会被保留。
最后的评论
在这些例子中,List [String]
被用作日志类型。但使用String
几乎从来不是最好的方式,只是一些日志框架强加给我们的惯例。最好定义一个自定义日志ADT,并在需要输出时尽可能晚地将其转换为字符串。这样,您可以序列化日志的ADT并随后轻松进行程序分析(而不是解析字符串)。
WriterT
有许多有用的方法可用于简化记录日志,请查看源代码。例如,给定一个w:WriterT [...]
,您可以使用w ++> List(“other event”)
添加新的日志条目,甚至可以使用当前持有的值进行日志记录w :++>> ((v) => List("the result is " + v))
等等。
在这些示例中有许多明确且冗长的代码(类型、调用)。与往常一样,这些是为了清晰明了,可以通过提取公共类型和操作来在您的代码中进行重构。
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")
MonadWriter
类型类。值得关注。 - ron