如何在Scala中进行单子日志记录

6

我经常想要记录或打印一些内容,而不改变它的形式。

这个操作通常是这样实现的:

val result = myResult // this could be an Option or a Future
      .map{r =>
        info(s"the result is $r")
        r
      }

这三行代码始终保持不变。

在for推导式中,可以更加优雅地实现这一点。

但我正在寻找第一种声明式版本的解决方案。它应该像这样:

val result = myResult
      .log(info(s"the result is ${_}"))

这个一行代码可以放在链中任何可能有map的地方,例如:
val result = myResult
      .log(info(s"1. ${_}"))
      .filter(_ > 1)
      .log(info(s"2. ${_}"))
      ...

如何实现呢?如果可能的话,不要使用功能库。

1
有趣...也许你可以定义一个隐式类,该类具有在产品上执行“log”操作的方法? - sinanspd
2
使用Cats时,可以使用.flatTap()来完成此操作。 - Marth
@Marth,我认为flatTap不适合像OP在最后一个示例中展示的那样进行链接。 - smac89
1
我本来想建议使用新的(Scala 2.13)tapEach(),但它在Future上不可用。我想知道为什么。也许在下一个版本中会有吧? - jwvh
1
@jwvh 当然很好奇为什么期货被不同对待。我会把这个问题带到/contributors,看看是否有人能解释一下。 - sinanspd
@jwvh 感谢您的建议。我尝试了 tapEach()。它只在 Iterable 中实现。在 Option 中使用它会返回一个 List - pme
3个回答

3

好的,我决定尝试一下,并且我想撤回我的评论:

也许您可以定义一个隐式类,该类具有作用于 Product 上的“log”方法?

我曾自信地认为 Future 和所有单子(集合、选项)都有共同的祖先,结果我错了。我有以下解决方案,而不使用 Cats。这在 cats 中可以以更漂亮的方式完成,除了前面提到的 "flatTap",还可以使用可能的 cats.Ref 等进行装饰。

未来在这里显然是个例外,但是随着更多的异常出现,您可能需要扩展此对象。

import scala.concurrent._ 
import ExecutionContext.Implicits.global

object MonadicConv { 
  implicit class MonadicLog[+B <: Product, A] (val u: B){
    def log(l: String, args: List[A] = List()): B = {
      println("logging")
      println(u)
      println(l)
      u
    }  
  }



 implicit class FutureLog[T, A](val u: Future[T]){
    def log(l: String, args: List[A] = List()) : Future[T] = {
      println("logging")
      println(u)
      println(l)
      u
    }
  }
}

1) 您需要使用自己的日志记录逻辑来修改此内容,我只是在打印。

2) 我对此并不感到非常自豪,因为它不再是一个纯函数。我不确定在Scala中是否有一种方法可以解决这个问题而不使用Cats。(可能有)

3) args可以被删除,只是添加了它们,以防您想要传递额外的信息。

4) 如果您真的想将它们合并,可以尝试定义自己的product,一些线索:在Scala中实现具有通用更新函数的产品类型,该函数适用于其部分

您可以使用此功能。

  import MonadicConv._


  val x = Some(5).log("").get
  val lx = List(Some(5), Some(10), Some(1)).log("list").flatten.log("x").filter(_ > 1).log("")
  val ff = Future.successful(List(Some(5), Some(10), Some(1))).log("fff").map(_.flatten.filter(_ > 1).log("inner")).log("")

这将打印出:

这将打印出

logging
Some(5)
option test
logging
List(Some(5), Some(10), Some(1))
list test
logging
List(5, 10, 1)
flat test
logging
List(5, 10)
filter test
logging
Future(Success(List(Some(5), Some(10), Some(1))))
future test
logging
Future(<not completed>)
incomplete test
logging
List(5, 10)
inner future test

在这里,我提到过,目前这真的是Cats领域。这是我在核心Scala中能想到的最好的解决方案。

Scastie版本在这里


1
谢谢,我不得不为Iterable添加一个版本,因为Product只涵盖了Try和Option。 - pme
是的。List是一种产品,但我忘记了它是一个特殊情况。希望Scala 3引入联合类型后,我们可以进一步简化这个问题。 - sinanspd

1

对于您的目的,最好使用treelog。它将记录过程和值转换为DescribedComputation的单子:

import treelog.LogTreeSyntaxWithoutAnnotations._
val result: DescribedComputation[YourValueType] = myResult ~> (_.fold("The result is empty")(r => s"The result is $r")

通常情况下,要从一个DescribedComputation中减去值,可以使用for comprehension:
for {
  res <- result
} {
  doSomethingTo(res)
}

请查看来自https://github.com/lancewalton/treelog的详细信息。


整个示例将如下所示:

val compRes = "Logging Result" ~< {
    for {
      r <- myResult ~> (_.fold("The result is empty")(r => s"The result is $r")
    } yield r
  }
}

for (res <- compRes) {
  doSomethingTo(res)
}

logger.info(logging.run.written.shows)

输出将会像这样:

内容

2019-11-18 00:00:00,000 INFO Logging Result
  The result is XXX

1

仅供参考。 ZIO 很好地提供了这个功能。

  /**
   * Returns an effect that effectfully "peeks" at the success of this effect.
   *
   * {{{
   * readFile("data.json").tap(putStrLn)
   * }}}
   */
  final def tap[R1 <: R, E1 >: E](f: A => ZIO[R1, E1, Any]): ZIO[R1, E1, A] = self.flatMap(new ZIO.TapFn(f))

甚至还有一个针对错误情况的版本:

  /**
   * Returns an effect that effectfully "peeks" at the failure of this effect.
   * {{{
   * readFile("data.json").tapError(logError(_))
   * }}}
   */
  final def tapError[R1 <: R, E1 >: E](f: E => ZIO[R1, E1, Any]): ZIO[R1, E1, A]

这使得调试变得非常容易:

   myDangerousZioFunction
      .tapError(e => putStrLn(s"Server Exception: $e"))
      .tap(r => putStrLn(s"Result is $r"))
      ....

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接