Функциональный scala накопитель журнала - PullRequest
1 голос
/ 06 мая 2020

Я работаю над проектом Scala, в основном, с использованием библиотеки cats. Там у нас есть такие вызовы, как

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

. Представьте, что b тоже что-то регистрирует или может выдать бизнес-ошибку (я знаю, мы избегаем вставлять Scala, но сейчас это не так). Я ищу решение, чтобы собирать логи и распечатывать их все в одном сообщении. Для счастливого пути я увидел, что Writer Monad из Cats может быть приемлемым решением. Но что, если метод b выбрасывает? Требования заключаются в том, чтобы регистрировать все - все предыдущие журналы и сообщение об ошибке в одном сообщении с каким-то уникальным идентификатором трассировки. Есть предположения? Заранее спасибо

1 Ответ

1 голос
/ 07 мая 2020

Реализовать функциональное ведение журнала (таким образом, чтобы журналы сохранялись даже в случае ошибки) с использованием преобразователей монад, таких как 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, поэтому я бы лично его использовал.

...