主要是我正在使用cats库进行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的作家Monad可能是可以接受的解决方案。 但是,如果b方法抛出异常怎么办?要求是将所有内容-所有以前的日志和错误消息都记录在一条消息中,并带有某种唯一的跟踪ID。 有什么想法吗?预先感谢
答案 0 :(得分:1)
使用诸如Writer(WriterT)或State(StateT)之类的monad转换器来实现功能日志记录(即使发生错误也可以保留日志)。但是,如果我们不了解FP方法,则可以执行以下操作:
我个人会选择cats.effect.concurrent.Ref
或monix.eval.TaskLocal
。
使用引用(和任务)的示例:
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)
...
}
}
但是,确保调用输出有点麻烦,因此我们可以使用某种形式的Bracket
或Resource
来处理它:
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)
}
如果您不喜欢明确地传递此追加器,则可以使用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
的使用方式非常相似。
最终,您将得到:
我相信有些纯粹主义者不会喜欢这种解决方案,但是它具有FP的所有优点,所以我会亲自使用它。