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

Key*_*nan 22 logging scala scalaz

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

ron*_*ron 34

关于monad变形金刚

这是一个非常简短的介绍.您可以在@jrwest找到有关haskellwiki或这个伟大幻灯片的更多信息.

Monads不构成,意味着如果你有monad A[_]和monad B[_],那么A[B[_]]就不能自动导出.然而,在大多数情况下,这可以通过为给定的monad具有所谓的monad变换器来实现.

如果我们有单子转换BT的单子B,那么我们就可以组成一个新的单子A[B[_]]任何单子 A.这是正确的,通过使用BT,我们可以把B内部A.

Monad变压器在scalaz中的使用

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

monad变换器MT有两个类型参数,第一个是包装器(外部)monad,第二个是monad堆栈底部的实际数据类型.注意:可能需要更多的类型参数,但这些参数与变换器无关,而是与给定的monad相关(如a的记录类型Writer或a的错误类型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进行日志记录

根据您的需要,您可以使用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)
Run Code Online (Sandbox Code Playgroud)

我们导入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))
Run Code Online (Sandbox Code Playgroud)

使用这种方法,由于日志记录在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)
Run Code Online (Sandbox Code Playgroud)

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

最后的评论

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

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

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