关于 Monad 变压器
这是一个非常简短的介绍。您可能会找到更多信息哈斯克尔维基 http://www.haskell.org/haskellwiki/Monad_Transformers_Explained或这个@jrwest 制作的很棒的幻灯片 https://speakerdeck.com/u/jrwest/p/monad-transformers.
Monad 不会组合,这意味着如果你有一个 monadA[_]
和一个单子B[_]
, then A[B[_]]
无法推导自动地。然而,在大多数情况下,这可以通过针对给定单子使用所谓的单子变压器来实现。
如果我们有 monad 变压器BT
对于单子B
,然后我们可以组成一个新的 monadA[B[_]]
for 任何单子 A
。没错,通过使用BT
,我们可以把B
inside A
.
Monad 变压器在 scalaz 中的使用
以下假设 scalaz 7,因为坦率地说,我没有在 scalaz 6 中使用 monad 转换器.
单子变压器MT
采用两个类型参数,第一个是包装器(外部)monad,第二个是 monad 堆栈底部的实际数据类型。注意:它可能需要更多类型参数,但这些参数与转换器性质无关,而是特定于给定的 monad(例如 a 的记录类型)Writer
,或a的错误类型Validation
).
所以如果我们有一个List[Option[A]]
我们想将其视为单个组合单子,那么我们需要OptionT[List, A]
。如果我们有Option[List[A]]
, 我们需要ListT[Option, A]
.
到那里怎么走?如果我们有非转换器值,我们通常可以将其包装为MT.apply
获取变压器内部的值。为了从转换后的形式恢复到正常形式,我们通常调用.run
关于变换后的值。
So val a: OptionT[List, Int] = OptionT[List, Int](List(some(1))
and 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
没有任何特定的外部单子(在这种情况下,在后台它将使用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
结果没有任何日志。
Note: 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
里面的单子Writer
。其中一项计算是None
以表明即使在这种情况下日志也会被保留。
最后的评论
在这些例子中List[String]
被用作日志类型。然而使用String
这几乎不是最好的方法,只是日志框架强加给我们的一些约定。例如,最好定义一个自定义日志ADT,如果需要输出,请尽可能晚地将其转换为字符串。这样您就可以序列化日志的 ADT,并在以后以编程方式轻松分析它(而不是解析字符串)。
WriterT
有许多有用的方法可以用来简化日志记录,请查看源代码。例如给定一个w: WriterT[...]
,您可以使用添加新的日志条目w :++> List("other event")
,或者甚至使用当前持有的值进行记录w :++>> ((v) => List("the result is " + v))
, etc.
示例中有许多显式且较长的代码(类型、调用)。与往常一样,这些都是为了清楚起见,通过提取常见类型和操作在代码中重构它们。