Представьте, что вы пишете функцию вычисления ряда Фибоначчи, подобную этой, и добавляете оператор печати для целей отладки:
Затем вы запускаете следующую функцию:
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.
Нравится эта статья?
Подпишитесь на мою рассылку новостей, чтобы получать этот контент еженедельно!