Представьте, что вы пишете функцию вычисления ряда Фибоначчи, подобную этой, и добавляете оператор печати для целей отладки:

Затем вы запускаете следующую функцию:

def fib(n:Int) : Int = {
  if(n == 0 || n ==1) {
    println(s"base case : $n")
    n
  }
  else {
    println(s"add fib(n-1) + fib(n-2) $n")
    fib(n-1) + fib(n-2)
  }
}

Условие одновременного выполнения нескольких функций, оно отлично работает:

fib(5)

// interpreter
add fib(n-1) + fib(n-2) 5
add fib(n-1) + fib(n-2) 4
add fib(n-1) + fib(n-2) 3
add fib(n-1) + fib(n-2) 2
base case : 1
base case : 0
base case : 1
add fib(n-1) + fib(n-2) 2
base case : 1
base case : 0
add fib(n-1) + fib(n-2) 3
add fib(n-1) + fib(n-2) 2
base case : 1
base case : 0
base case : 1
fib: (n: Int)Int
res0: Int = 5

Однако, если функция обернута асинхронным образом, будет действительно сложно сказать, какой журнал связан с каким шагом. Как в этом сценарии отлаживать асинхронную операцию?

Типы данных Writer спешат на помощь

У кошек есть класс типа данных писателя, который может помочь вам, прикрепив оператор журнала к базовому значению результата, чтобы вы могли понимать операторы журнала асинхронно.

В Cats определение типов данных Writer: Writer[L, V].

L - это набор типов журналов Monoid, который вы хотите иметь; в этом случае мы можем установить L как Vector[String].

V - это тип результата операции функции - в этом случае мы можем установить V как Int, поскольку мы возвращаем целочисленный тип из Фибоначчи.

Инициализация

Как только вы создадите информацию о L и V в определении, вы можете создать свой тип данных Writer следующим образом:

import cats.data.Writer
import cats.implicits._

Writer(Vector("log1", "log2"), 0)

or

import cats.data.Writer
import cats.implicits._

0.writer(Vector("log1", "log2"))

Если вы видели ответ или результат, вы поймете, что это не Writer[L, V], а верните WrtierT[Id, L, V]. Это потому, что кошки используют псевдоним типа для получения значения Writer от WriterT. В этом посте мы поговорим о том, как использовать Writer. Следовательно, вы можете игнорировать детали и рассматривать тип WriterT[Id, L, V] как Writer[L, V].

Имеется значение журнала, но нет результата

Если есть журнал, но нет результата, мы можем использовать tell:

Vector("msg1", "msg2").tell()

Мы также можем извлекать и вывод, и журналы одновременно с run:

val writer = Writer(Vector("something"), 0)
val (log, result) = writer.run

Извлечь значение результата и тип журнала

Извлеките результат и журнал с value и written соответственно:

val a = Writer(Vector("msg1"),0)
val log = a.written
val result = a.value

println(s"log: $log result: $result")
// log: Vector(msg1) result: 0

Сочинение и преобразование писателей

Поскольку Writer - это монада, вы можете выполнять операции с Writer с помощью map и flatmap.

flatMap объединяет тип журнала, а также тип результата из исходного Writer и результат функции упорядочивания.

Поэтому рекомендуется использовать тип журнала, который имеет эффективный метод добавления и объединения, например Vector:

val res = for {
    a <- Writer(Vector("a"), 1)
    _ <- Vector("c").tell
    b <- 3.writer(Vector("3", "b"))
  } yield {
    println(s"a $a") // 1
    println(s"b $b") // 3
    a + b // 4
  }

println(res) //WriterT((Vector(a, c, 3, b),4))

Обратите внимание, что метод tell сохранит исходный Writer и добавит «c» к исходному Writer, то есть «a».

Результат Writer основан на том, что будет вычислено после функции yield. Если после yield нет добавления, а, например, только a, конечным результатом будет WriterT((Vector(a,c,3,b),1)).

Писатель-трансформер

Мы можем изменить тип журнала на все верхний регистр, используя mapWritten:

// .. take example from previous res example
val upperCaseLog = res.mapWritten(previousLog => previousLog.map(_.toUpperCase))
  println(upperCaseLog) 
  // WriterT((Vector(A, C, 3, B),4))

Вы также можете преобразовать оба типа, используя mapBoth:

val newWriterValueAndLog = res.mapBoth{ (log,res) =>
    (log :+ "appending z", res+12)
  }
println(newWriterValueAndLog)

WriterT((Vector(a, c, 3, b, appending z),16))

Поменяйте местами тип журнала и результат, используя swap:

val swappedWriter = res.swap
println(swappedWriter)
// WriterT((4,Vector(a, c, 3, b)))

И последнее, но не менее важное: сбросьте значение журнала в Writer, используя reset:

val resetWriter = res.reset
println(resetWriter)

// WriterT((Vector(),4))

Писатель в действии

Теперь, когда вы дочитали это до конца и знаете, что такое Writer, давайте проведем рефакторинг нашего кода, чтобы включить в него Writer:

Во-первых, давайте добавим функцию timeOut для настройки асинхронной среды.

def timeout[A](body: => A):A = try {
    body
  } finally Thread.sleep(100)

Затем мы устанавливаем псевдоним типа LogFib из Writer:

type LogFib[A] = Writer[Vector[String], A]

Мы меняем функцию Фибоначчи, чтобы она возвращала LogFib[Int]:

def fib(n:Int): LogFib[Int] = {
    timeout(
      if(n == 0 || n ==1) {
        n.writer(Vector(s"base case : $n"))
      }
      else {
        for {
          _ <- Vector(s"add fib(n-1) + fib(n-2) $n").tell
          fib1 <- fib(n-1)
          fib2 <- fib(n-2)
        } yield fib1 + fib2
      }
    )
  }

Тогда вы можете запустить его так:

import scala.concurrent.duration._
  implicit val ec :ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
  val fibRes = Await.result(Future.sequence(Vector(
    Future(fib(5)),
    Future(fib(4)),
    Future(fib(3))
  ))
  , Duration.Inf)


  fibRes.toList.map(w => {
    val (logging, endResult) = w.run
    println(s"logging $logging endResult $endResult")
  })

Забрать

  • Тип данных Writer полезен для операции ведения журнала в многопоточной среде.
  • Журнал Writer привязан к результату. Следовательно, это отличный способ записать последовательность многопоточных вычислений.

Вся информация о примерах находится в Github.

Нравится эта статья?

Подпишитесь на мою рассылку новостей, чтобы получать этот контент еженедельно!