对于for
理解中的日志跟踪,我使用了这样的虚拟赋值:
val ll = List(List(1,2),List(1))
for {
outer <- ll
a = Console.println(outer) // Dummy assignment makes it compile
inner <- outer
} yield inner
a =
位看起来很尴尬。有更干净的方式吗?
你总是可以定义自己的trace
函数:
def trace[T](x: T) = {
println(x) // or your favourite logging framework :)
x
}
那么for comprehension看起来像:
for {
outer <- ll
inner <- trace(outer)
} yield inner
或者,如果您想要打印更多信息,可以按如下方式定义trace
:
def trace[T](message: String, x: T) = {
println(message)
x
}
并且理解力看起来像:
for {
outer <- ll
inner <- trace("Value: " + outer, outer)
} yield inner
编辑:回应你的评论,是的,你可以写trace
,使其作用于目标的权利!你只需要使用一些隐含的技巧。实际上,它确实看起来比应用于左边时更好:)。
为此,您必须首先定义一个Traceable
类,然后定义到该类的隐式转换:
class Traceable[A](x: A) {
def traced = {
println(x)
x
}
}
implicit def any2Traceable[A](x: A) = new Traceable(x)
然后,您在提供的代码中唯一需要修改的是将traced
添加到要跟踪的值的末尾。例如:
for {
outer <- ll
inner <- outer traced
} yield inner
(这是由Scala编译器翻译成outer.traced
)
你的问题的简短回答是WriterT
monad变压器。答案如下。
在下面的解释中,我将为您提供一种工具,以实现您期望的目标,但使用与已经陈述的机制截然不同的机制。我将就最终的分歧的优点提出我的简短意见。
首先,什么是理解?理解是(对于我们的目的来说足够了)一个单子理解但具有不同的名称。这恰好是一个共同的主题;例如,C#有LINQ。
出于我们的解释目的(这不完全正确,但现在足够真实),monad是M
实现以下特征的任何值:
trait Monad[M[_]] {
def flatMap[A, B](a: M[A], f: A => M[B]): M[B]
def map[A, B](a: M[A], f: A => B): M[B]
}
也就是说,如果你有一个M的Monad实现,那么你可以对任何A值使用类型为M [A]的值进行求解。
适合此接口且位于标准库中的M值的一些示例是List
,Option
和Parser
。当然,你可能总是使用他们的理解。其他示例可能是您自己的数据类型。例如:
case class Inter[A](i: Int => A)
......这里是Monad
的Inter
实现:
val InterMonad: Monad[Inter] = new Monad[Inter] {
def flatMap[A, B](a: Inter[A], f: A => Inter[B]) =
Inter(n => f(a.i(n)).i(n))
def map[A, B](a: Inter[A], f: A => B) =
Inter(n => f(a.i(n)))
}
M的价值还有很多。问题是,基本上,我们如何为这些值添加日志记录支持?
Writer
数据类型只是一对(scala.Tuple2
)。在这一对中,我们计算一些值(让我们称之为A
)并将另一个值与它相关联(让我们称之为LOG
)。
// simply, a pair
case class Writer[LOG, A](log: LOG, value: A)
当我们计算值时,我们希望将日志值附加到当前计算的日志。在我们开始计算任何东西之前,我们希望有一个空日志。我们可以在界面中表示这些操作(append
和empty
):
trait Monoid[A] {
def append(a1: A, a2: A): A
def empty: A
}
有些法律规定此接口的所有实现都必须遵循:
作为旁注,这些也是Monad
接口的实现必须遵循的相同法则,但是我已经将它们排除在外以避免混淆并保持记录点。
这个Monoid
接口的实现有很多例子,其中一个是List:
def ListMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] {
def append(a1: List[A], a2: List[A]) =
a1 ::: a2
def empty =
Nil
}
只是为了标记这个Monoid
接口的多样性,这是另一个实现示例:
def EndoMonoid[A]: Monoid[A => A] = new Monoid[A => A] {
def append(a1: A => A, a2: A => A) =
a1 compose a2
def empty =
a => a
}
我知道这些概括可能会让你有点难以理解,所以我现在要做的是,专注于Writer
使用List
的String
值作为其日志。听起来合理吗?但是,有几点需要注意:
List
,因为它的append
不合需要的算法复杂性。相反,我们可能会使用基于手指树的序列或其他在结束操作时插入速度更快的序列。List[String]
只是Monoid
实现的一个例子。重要的是要记住,存在大量其他可能的实现,其中许多不是集合类型。请记住,我们需要的是任何Monoid
附加日志值。这是我们专门研究Writer
的新数据类型。
case class ListWriter[A](log: List[String], value: A)
这有什么意义呢?这是一个单子!重要的是,它的Monad
实现跟踪我们的日志记录,这对我们的目标很重要。让我们写一下实现:
val ListWriterMonad: Monad[ListWriter] = new Monad[ListWriter] {
def flatMap[A, B](a: ListWriter[A], f: A => ListWriter[B]) = {
val ListWriter(log, b) = f(a.value)
ListWriter(a.log ::: log /* Monoid.append */, b)
}
def map[A, B](a: ListWriter[A], f: A => B) =
ListWriter(a.log, f(a.value))
}
请注意flatMap
实现中附加了记录值。接下来,我们需要一些辅助函数来附加日志值:
def log[A](log: String, a: A): ListWriter[A] =
ListWriter(List(log), a)
def nolog[A](a: A): ListWriter[A] =
ListWriter(Nil /* Monoid.empty */, a)
......现在让我们看看它的实际效果。下面的代码与for-comprehension类似。但是,我们不是将值拉出并将它们命名为<-
的左侧,而是将flatMap值设置为右侧。我们使用我们定义的显式函数调用而不是for-comprehension:
val m = ListWriterMonad
val r =
m flatMap (log("computing an int", 42), (n: Int) =>
m flatMap (log("adding 7", 7 + n), (o: Int) =>
m flatMap (nolog(o + 3), (p: Int) =>
m map (log("is even?", p % 2 == 0), (q: Boolean) =>
!q))))
println("value: " + r.value)
println("LOG")
r.log foreach println
如果运行此小片段,您将看到最终计算值和计算发生时累积的日志。重要的是,您可以在任何时候拦截此计算并观察当前日志,然后通过利用表达式及其子表达式的引用透明属性来继续计算。请注意,在整个计算过程中,您还没有执行任何副作用,因此您保留了程序的组成属性。
您可能还想在map
上实现flatMap
和ListWriter
,它将复制Monad
实现。我将离开为你做这个:)这将允许你使用for-comprehension:
val r =
for {
n <- log("computing an int", 42)
o <- log("adding 7", 7 + n)
p <- nolog(o + 3)
q <- log("is even?", p % 2 == 0)
} yield !q
println("value: " + r.value)
println("LOG")
r.log foreach println
就像非记录值只是为了理解!
Righto,那么我们如何将这种记录功能添加到我们现有的for-understanding中呢?这是你需要WriterT
monad变压器的地方。同样,我们将专门用于List
的日志记录和演示目的:
// The WriterT monad transformer
case class ListWriterT[M[_], A](w: M[ListWriter[A]])
此数据类型将记录添加到在M
的任何值内计算的值。它通过自己的Monad
实现来实现这一点。不幸的是,这需要部分类型的构造函数应用程序,这很好,除了Scala不能很好地执行此操作。至少,它有点嘈杂,需要一些手工操作。在这里,请耐心等待:
def ListWriterTMonad[M[_]](m: Monad[M]):
Monad[({type λ[α]=ListWriterT[M, α]})#λ] =
new Monad[({type λ[α]=ListWriterT[M, α]})#λ] {
def flatMap[A, B](a: ListWriterT[M, A], f: A => ListWriterT[M, B]) =
ListWriterT(
m flatMap (a.w, (p: ListWriter[A]) =>
p match { case ListWriter(log1, aa) =>
m map (f(aa).w, (q: ListWriter[B]) =>
q match { case ListWriter(log2, bb) =>
ListWriter(log1 ::: log2, bb)})
}))
def map[A, B](a: ListWriterT[M, A], f: A => B) =
ListWriterT(
m map (a.w, (p: ListWriter[A]) =>
p match { case ListWriter(log, aa) =>
ListWriter(log, f(aa))
}))
}
这个monad实现的重点是,只要M
有Monad
,就可以将记录附加到任何值M
。换句话说,这就是你如何“在for-comprehension中添加跟踪”。 Monad
实现将自动处理附加日志值的处理。
出于解释的目的,我们偏离了如何实施这样的库以供实际使用。例如,当我们对Monad
使用ListWriterT
实现时,我们可能会坚持使用for-comprehension。但是,我们没有直接(或间接)实施flatMap
或map
方法,所以我们不能这样做。
尽管如此,我希望这个解释传达了WriterT
monad变换器如何解决你的问题。
现在,简要介绍一下这种方法的优点和可能的缺点。
虽然上面的一些代码可能非常抽象甚至是嘈杂,但它在计算值时封装了日志记录的代数概念。专门为实现这一目的而设计的库将尽可能减轻客户端代码的负担。巧合的是,几年前,当我在做一个商业项目时,我已经为Scala实现了这样一个库。
以这种方式记录的关键是将典型的副作用(例如打印或写入日志文件)与具有关联日志的值的计算分开,并为调用客户端自动处理日志记录的monoidal属性。最终,这种分离导致代码更易于阅读和推理(信不信由你,尽管存在一些语法噪音)并且不易出错。此外,它通过组合高级抽象函数来协助代码重用,以生成越来越多的专用函数,直到最终您处于特定应用程序的级别。
这种方法的缺点是它不适合程序崩溃。也就是说,如果您作为程序员尝试使用类型检查器或运行时解析参数,那么您可能希望使用调试断点或print
语句。相反,我给出的方法更适合登录生产代码,其中假设代码中没有矛盾或错误。
我希望这有帮助!
Here是关于该主题的相关帖子。
无论它值多少,因为赋值是虚拟的,你可以用a
替换_
:
for {
outer <- ll // ; // semi-colon needed on Scala 2.7
_ = Console.println(outer) // dummy assignment makes it compile
inner <- outer
} yield inner
Flaviu的回答激发了我尝试玩弄隐含物。我们的想法是看看跟踪看起来是否更好,线条右侧的“跟踪”:
import Trace._
object Main {
def main(args:Array[String]) {
val listList = List(List(1,2,3), List(3,4))
for {
list <- trace1(listList, "lList is: %s", listList) // trace()
item <- list traced("list is: %s", list) // implicit
} yield item
我也想尝试在相同的理解中混合错误记录。错误记录看起来最好与Daniel的方法混合:
val optOpt:Option[Option[Int]] = Some(Some(1))
for {
opt <- optOpt;
_ = trace2("opt found: %s", opt) // trying Daniel's suggestion
int <- opt orElse
err("num not found in: %s", opt) // together with error logging
} yield int
}
}
以下是两个实验的支持代码:
object Trace {
def trace1[T](any:T, message:String, params:AnyRef*):T = {
Console println String.format("TRA: " + message, params:_*)
any
}
def trace2[T](message:String, params:AnyRef*) {
Console println String.format("TRA: " + message, params:_*)
}
def err[T](message:String, params:AnyRef*):Option[T] = {
Console println String.format("ERR: " + message, params:_*)
None
}
implicit def anyRefToTraceable[T](anyRef:T):Traceable[T] = {
new Traceable(anyRef)
}
class Traceable[T](val self:T) {
def traced(message:String, params:AnyRef*):T = {
Console println String.format("TRA: " + message, params:_*)
self
}
}
}
启动Scala 2.13
,链接操作tap
已包含在标准库中,并且只要我们需要打印管道的某个中间状态,就可以使用最小的侵入性:
import util.chaining._
// val lists = List(List(1, 2), List(1))
for {
outer <- lists
inner <- outer.tap(println)
} yield inner
// List(2, 4, 6)
// List(4, 8, 12)
// ls: List[Int] = List(4, 8, 12)
tap
链接操作对值(在本例中为println
列表)应用副作用(在这种情况下为outer
),同时返回此值不变:
def tap [U](f:(A)=> U):A