【问题标题】:Functional scala log accumulator功能性Scala日志累加器
【发布时间】:2020-05-06 13:41:08
【问题描述】:

我主要使用猫库来开发一个 Scala 项目。在那里,我们有类似的电话

for {
   _ <- initSomeServiceAndLog("something from a far away service")
   _ <- initSomeOtherServiceAndLog("something from another far away service")
   a <- b()
   c <- d(a)
  } yield c

想象一下 b 也记录了一些东西或者可能抛出一个业务错误(我知道,我们避免在 Scala 中抛出,但现在情况并非如此)。我正在寻找一种解决方案来累积日志并最终在一条消息中将它们全部打印出来。 对于一条幸福的道路,我看到 Cats 的 Writer Monad 可能是一个可以接受的解决方案。 但是如果 b 方法抛出怎么办?要求是在一条消息中记录所有内容——所有以前的日志和错误消息,并带有某种唯一的跟踪 ID。 有什么想法吗?提前致谢

【问题讨论】:

  • 这比听起来要难。 Writer[L, A] 基本上是一个允许组合函数的包装器:A =&gt; (L, A2) 这样L 将被传递并有时附加到。 WriterT[F, L, A] 将允许 A =&gt; F[(L A2)] - 这意味着如果 F 中的值变为失败(因为它是例如 TryFuture 或其他 IO),那么到目前为止所有结果都会消失。您将不得不引入一个新的 monad,它代表 A =&gt; (L, F[A2])... 但这比听起来要难。就我个人而言,我可能会选择 Ref(来自 Cats Effect)或 TaskLocal(来自 Monix)并写信给它。
  • 您能否详细说明在方法抛出时如何使用 Ref 来控制日志累加器?
  • 如何将 WriteT 与 Either 作为 F 一起使用。确保您的 Either 捕获所有内容。

标签: scala logging scala-cats


【解决方案1】:

使用像 Writer (WriterT) 或 State (StateT) 这样的 monad 转换器来实现功能性日志记录(以一种即使发生错误也能保留日志的方式)是很困难的。但是,如果我们不了解 FP 方法,我们可以执行以下操作:

  • 使用一些 IO monad
  • 使用它为日志创建类似内存存储的东西
  • 但是以功能方式实现

我个人会选择cats.effect.concurrent.Refmonix.eval.TaskLocal

使用 Ref(和 Task)的示例:

type Log = Ref[Task, Chain[String]]
type FunctionalLogger = String => Task[Unit]
val createLog: Task[Log] = Ref.of[Task, Chain[String]](Chain.empty)
def createAppender(log: Log): FunctionalLogger =
  entry => log.update(chain => chain.append(entry))
def outputLog(log: Log): Task[Chain[String]] = log.get

有了这样的助手,我可以:

def doOperations(logger: FunctionalLogger) = for {
  _ <- operation1(logger) // logging is a side effect managed by IO monad
  _ <- operation2(logger) // so it is referentially transparent
} yield result

createLog.flatMap { log =>
  doOperations(createAppender(log))
    .recoverWith(...)
    .flatMap { result =>
       outputLog(log)
       ...
    }
}

但是,确保调用输出有点麻烦,因此我们可以使用某种形式的 BracketResource 来处理它:

val loggerResource: Resource[Task, FunctionalLogger] = Resource.make {
  createLog // acquiring resource - IO operation that accesses something
} { log =>
  outputLog(log) // releasing resource - works like finally in try-catchso it should
    .flatMap(... /* log entries or sth */) // be called no matter if error occured
}.map(createAppender)

loggerResource.use { logger =>
  doSomething(logger)
}

如果你不喜欢明确地传递这个 appender,你可以使用 Kleisli 来注入它:

type WithLogger[A] = Kleisli[Task, FunctionalLogger, A]

// def operation1: WithLogger[A]
// def operation2: WithLogger[B]

def doSomething: WithLogger[C] = for {
  a <- operation1
  b <- operation2
} yield c

loggerResource.use { logger =>
  doSomething(logger)
}

TaskLocal 的使用方式非常相似。

最终你会得到:

  • 表示正在记录的类型
  • 通过 IO 管理可变性,因此不会丢失引用透明度
  • 确定即使 IO 失败,也会保留日志并发送结果

我相信有些纯粹主义者不会喜欢这种解决方案,但它具有 FP 的所有优点,所以我会亲自使用它。

【讨论】:

    猜你喜欢
    • 2020-05-01
    • 2015-06-08
    • 2018-03-23
    • 1970-01-01
    • 2014-09-18
    • 1970-01-01
    • 1970-01-01
    • 2019-03-02
    • 1970-01-01
    相关资源
    最近更新 更多