Реализовать функциональное ведение журнала (таким образом, чтобы журналы сохранялись даже в случае ошибки) с использованием преобразователей монад, таких как Writer (WriterT) или State (StateT), сложно. Однако, если мы не задумываемся о подходе FP, мы могли бы сделать следующее:
- использовать некоторую монаду ввода-вывода
- с ней создать что-то вроде хранилища в памяти для журналов
- однако реализовать функционально
Лично я бы выбрал cats.effect.concurrent.Ref
или monix.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)
...
}
}
Однако убедиться, что вывод вызывается, немного затруднительно, поэтому мы могли бы использовать некоторую форму 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
будет использоваться в очень аналогично.
В конце дня вы получите:
- тип, который говорит, что он регистрирует
- изменчивость, управляемая через ввод-вывод, так что ссылочная прозрачность не будет потеряна
- уверенность в том, что даже в случае сбоя ввода-вывода журнал будет сохранен, а результаты будут отправлены * 10 39 *
Я считаю, что некоторым пуристам не понравится это решение, но оно имеет все преимущества FP, поэтому я бы лично его использовал.