您如何使用 scalaz.WriterT 进行日志记录?
2 回答
关于单子变压器
这是一个非常简短的介绍。您可以在haskellwiki或@jrwest 的这张精彩幻灯片上找到更多信息。
Monads 不组合,这意味着如果你有一个 monadA[_]
和一个 monad B[_]
,那么A[B[_]]
就不能自动派生。然而,在大多数情况下,这可以通过为给定的 monad 使用所谓的 monad 转换器来实现。
如果我们有 monad 的 monad 转换BT
器B
,那么我们可以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 中登录
在这里,为了简单起见,我们选择了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))
使用这种方法,由于日志记录在Option
monad 内部,如果任何绑定选项是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)
在这里,我们使用OptionT
将Option
monad 放在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")