如何在'for'理解中添加跟踪?

Lee*_*oll 25 logging scala

对于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
Run Code Online (Sandbox Code Playgroud)

a =一点似乎很尴尬.有更干净的方式吗?

Ton*_*ris 71

简短的回答你的问题是WriterT单子转换.答案如下.

在下面的解释中,我将为您提供一种工具,以实现您期望的目标,但使用与已经陈述的机制截然不同的机制.我将就最终的分歧的优点提出我的简短意见.

首先,什么是理解?理解是(对于我们的目的来说足够了)一个单子理解但具有不同的名称.这恰好是一个共同的主题; 例如,C#有LINQ.

什么是monad?

出于我们的解释目的(这不完全正确,但现在已经足够了),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]
}
Run Code Online (Sandbox Code Playgroud)

也就是说,如果你有一个M的Monad实现,那么你可以对任何A值使用类型为M [A]的值进行求解.

适合此接口且位于标准库中的M值的一些示例是List,OptionParser.当然,你可能总是使用他们的理解.其他示例可能是您自己的数据类型.例如:

case class Inter[A](i: Int => A) 
Run Code Online (Sandbox Code Playgroud)

...这里是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)))
}
Run Code Online (Sandbox Code Playgroud)

有很多很多的M.更多的价值,你有现在的问题是,从本质上讲,我们怎么加记录支持这些价值观?

Writer数据类型

Writer数据类型是一个简单的对(scala.Tuple2).在这一对中,我们计算一些值(让我们称之为A)并将另一个值与它相关联(让我们称之为LOG).

// simply, a pair
case class Writer[LOG, A](log: LOG, value: A)
Run Code Online (Sandbox Code Playgroud)

当我们计算值时,我们希望日志值附加到当前计算的日志.在我们开始计算任何东西之前,我们希望有一个日志.我们可以在一个接口中表示这些操作(appendempty):

trait Monoid[A] {
  def append(a1: A, a2: A): A
  def empty: A
}
Run Code Online (Sandbox Code Playgroud)

有些法律规定此接口的所有实现都必须遵循:

  • 相关性: append(x,append(y,z))== append(append(x,y),z)
  • 正确的身份:追加(空,x)== x
  • 左标识: append(x,empty)== x

作为旁注,这些也是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
}
Run Code Online (Sandbox Code Playgroud)

只需标记此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
}
Run Code Online (Sandbox Code Playgroud)

据我所知,这些概括可能会变得有点难以保持在你的脑袋,所以我今天尽现,是一家专业的Writer使用ListString值,它的日志.听起来合理吗?但是,有几点需要注意:

  1. 在实践中,我们不会使用它,List因为它的算法复杂度不合理append.相反,我们可能会使用基于手指树的序列或其他在结束操作时插入速度更快的序列.
  2. List[String]只是一个Monoid实现的例子.重要的是要记住,存在大量其他可能的实现,其中许多不是集合类型.请记住,我们需要的只是Monoid附加日志值.

这是我们专门研究的新数据类型Writer.

case class ListWriter[A](log: List[String], value: A)
Run Code Online (Sandbox Code Playgroud)

这有什么意义呢?这是一个单子!重要的是,它的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))
} 
Run Code Online (Sandbox Code Playgroud)

请注意在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)
Run Code Online (Sandbox Code Playgroud)

......现在让我们看看它的实际效果.下面的代码与for-comprehension类似.但是,我们不是将值拉出并将它们命名为a的左侧<-,而是将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
Run Code Online (Sandbox Code Playgroud)

如果运行此小片段,您将看到最终计算值和计算发生时累积的日志.重要的是,您可以在任何时候拦截此计算并观察当前日志,然后通过利用表达式及其子表达式的引用透明属性来继续计算.请注意,在整个计算过程中,您还没有执行任何副作用,因此您保留了程序的组成属性.

您可能还想实现mapflatMapListWriter其上复制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
Run Code Online (Sandbox Code Playgroud)

就像非记录值只是为了理解!

WriterT Monad变压器

Righto,那么我们如何将这种记录功能添加到我们现有的for- understanding中呢?这是你需要WriterTmonad变压器的地方.同样,我们将专门List用于记录和演示目的:

// The WriterT monad transformer
case class ListWriterT[M[_], A](w: M[ListWriter[A]])
Run Code Online (Sandbox Code Playgroud)

此数据类型将记录添加到在任何值内计算的值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))
      }))
  }
Run Code Online (Sandbox Code Playgroud)

这个单子实现的一点是,你可以将日志记录到任何值M,只要有一个对MonadM.换句话说,这就是你如何"在for-comprehension中添加跟踪".实现将自动处理附加日志值的处理Monad.

出于解释的目的,我们偏离了如何实施这样的库以供实际使用.例如,当我们使用Monad实现时,ListWriterT我们可能会坚持使用for-comprehension.但是,我们没有直接(或间接)实施flatMapmap方法,所以我们不能这样做.

尽管如此,我希望这个解释能够传达出WriterTmonad变换器如何解决你的问题.

现在,简要介绍一下这种方法的优点和可能的缺点.

批判

虽然上面的一些代码可能非常抽象甚至是嘈杂,但它在计算值时封装了日志记录的代数概念.专门为实现这一目的而设计的库将尽可能减轻客户端代码的负担.巧合的是,几年前,当我在做一个商业项目时,我已经为Scala实现了这样一个库.

以这种方式记录的关键是将典型的副作用(例如打印或写入日志文件)与具有关联日志的值的计算分开,并为调用客户端自动处理日志记录的monoidal属性.最终,这种分离导致代码更容易阅读和推理(不管信不信,尽管有一些语法噪音)并且不易出错.此外,它通过组合高级抽象函数来协助代码重用,以生成越来越多的专用函数,直到最终您处于特定应用程序的级别.

这种方法的缺点是它不适合程序崩溃.也就是说,如果您作为程序员尝试使用类型检查器或运行时解析参数,那么您可能希望使用调试断点或print语句.相反,我给出的方法更适合登录生产代码,其中假设代码中没有矛盾或错误.

结论

我希望这有帮助!

是关于该主题的相关帖子.


Fla*_*gan 21

您可以随时定义自己的trace功能:

def trace[T](x: T) = {
  println(x) // or your favourite logging framework :)
  x
}
Run Code Online (Sandbox Code Playgroud)

那么for comprehension看起来像:

for { 
  outer <- ll
  inner <- trace(outer)
} yield inner
Run Code Online (Sandbox Code Playgroud)

或者,如果您想要打印更多信息,可以定义trace如下:

def trace[T](message: String, x: T) = {
  println(message)
  x
}
Run Code Online (Sandbox Code Playgroud)

并且理解力看起来像:

for { 
  outer <- ll
  inner <- trace("Value: " + outer, outer)
} yield inner
Run Code Online (Sandbox Code Playgroud)

编辑:回应你的评论,是的,你可以写,trace以便它作为目标的权利!你只需要使用一些隐含的技巧.实际上,它确实看起来比应用于左边时更好:).

为此,您必须先定义一个类,Traceable然后定义对该类的隐式转换:

class Traceable[A](x: A) { 
  def traced = {
    println(x)
    x
  }
}

implicit def any2Traceable[A](x: A) = new Traceable(x)
Run Code Online (Sandbox Code Playgroud)

然后,您在提供的代码中唯一要修改的是添加traced到要跟踪的值的末尾.例如:

for { 
  outer <- ll
  inner <- outer traced
} yield inner
Run Code Online (Sandbox Code Playgroud)

(这是由Scala编译器翻译成的outer.traced)


Dan*_*ral 16

无论它是什么值,因为赋值是虚拟的,你可以替换a_:

for { 
  outer <- ll  // ; // semi-colon needed on Scala 2.7
  _ = Console.println(outer)   // dummy assignment makes it compile 
  inner <- outer 
} yield inner 
Run Code Online (Sandbox Code Playgroud)