Я с трудом понимаю, для чего нужны кошки flatTap?
def flatTap[B](f: A => F[B]): F[A] = typeClassInstance.flatTap[A, B](self)(f)
На самом деле я просто пытался использовать значение эффекта для регистрации, а затем возвращать этот эффект.
Кажется, что scala 2.13 tap — это то, что нужно. Однако с таким эффектом, как Либо, нам нужно протестировать вещи:
(Right(2).withLeft[Throwable]).tap(....)
def tap[U](f: A => U): A = {
f(self)
self
}
любое предложение о том, как я могу добиться этого, не будучи слишком изощренным, например, представляя расширенную библиотеку и тому подобное....
Представьте, что вы делаете некоторые побочные эффекты:
def fetchUser(id: ID): F[User] // side effect - fetching from DB
def modifyUser(user: User): User // pure computation
def saveUser(user: User): F[Unit] // side effect - storing in DB
Что произойдет, если вы извлечете, затем модифицируете, а затем попытаетесь сохранить модификацию?
fetchUser(userID).map(modifyUser).flatMap(saveUser)
Вы бы получили F[Unit]
. User
будет обновлен, но вы потеряете к нему доступ.
Итак, вы можете подумать, что можете сделать что-то вроде:
fetchUser(userID).map(modifyUser).flatMap { modified =>
saveUser(modified).map(_ => modified)
}
За исключением того, что было бы неудобно делать это каждый раз, когда вы хотите запустить вычисление, которое не должно влиять на возвращаемое значение. Вот почему у нас есть flatTap
fetchUser(userID).map(modifyUser).flatTap(saveUser) // F[User]
Чем он отличается от tap
?
Допустим, ваш F
= cats.effect.IO
.
Оба:
fetchUser(userID).map(modifyUser).flatTap(saveUser)
и
fetchUser(userID).map(modifyUser(_).tap(saveUser))
будет иметь тот же тип.
Однако последняя часть будет иметь другое значение:
user =>
saveUser(u).map(_ => u)
user =>
saveUser(u) // lazy computation!!!
user // saveUser value is discarded!!!
Таким образом, flatTap
позаботится о том, чтобы saveUser
вычисления были частью плана программы, в то время как .tap
создаст рецепт вычислений и отбросит его, не вписывая в программу.
Типичным случаем использования .tap
является то, что вы хотите выполнить операцию, возвращаемое значение которой можно безопасно отбросить, не нарушая вашей логики. Обычно это Unit
возвращается немедленной побочной операцией:
// - we dont' want to return Unit
// - we can discard it and return original value
// - println is _eager_ so the side effects will happen immediately
// and so we won't skip them if we discard the value
calculateSomething().tap { value =>
println(value)
}
// each setX returns Unit so we couldn't chain them together
// (no .setA(...).setB(...)) but we can chain .taps
val value = new JavaObject()
.tap(_.setA("value"))
.tap(_.setB(1))
В контексте ведения журнала, нужно ли вам .tap
или .flatTap
, зависит от того, какое решение для ведения журнала вы используете: тот, который регистрирует немедленно (Log4j, Slf4J, Scala Logging, ...) или тот, который возвращает F[Unit]
для последующей оценки (Log4Cats). Типы должны направлять вас (вы не можете вернуть Unit
в .flatTap
и не можете .tap
в F[X]
и ожидать получить X
), но если у вас есть сомнения, просто поиграйтесь с кодом и посмотрите, чем поведение нечистых вычислений отличается от ссылочно-прозрачных.