Imagine you do some side effects:
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
What would happen if you fetched, then modified, then tried to save the modification?
fetchUser(userID).map(modifyUser).flatMap(saveUser)
You would get F[Unit]. User would be updated but you would lost access to it.
So, you might think that you could do something like:
fetchUser(userID).map(modifyUser).flatMap { modified =>
saveUser(modified).map(_ => modified)
}
Except it would be annoying to do this every time you want to trigger a computation which shouldn't affect the returned value. This is why we have flatTap
fetchUser(userID).map(modifyUser).flatTap(saveUser) // F[User]
How does it differ from tap?
Let's say your F = cats.effect.IO.
Both:
fetchUser(userID).map(modifyUser).flatTap(saveUser)
and
fetchUser(userID).map(modifyUser(_).tap(saveUser))
would have the same type.
However the last part would be different value:
user =>
saveUser(u).map(_ => u)
user =>
saveUser(u) // lazy computation!!!
user // saveUser value is discarded!!!
So flatTap would make sure that saveUser computation is part of the program blueprint, while .tap would create a recipe for computation and discard it without composing it into the program.
The typical use case for .tap is that you want to perform an operation which returned value can be discarded safely, without breaking your logic. Usually, it's Unit returned by immediately side-effecting operation:
// - 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))
In context of logging whether you need .tap or .flatTap depends on which logging solution you use: one that logs immediately (Log4j, Slf4J, Scala Logging, ...) or one which returns F[Unit] to be evaluated later (Log4Cats). Types should guide you (you cannot return Unit in .flatTap and you cannot .tap on F[X] and expect to receive X) but if you have doubts just play around with the code and see how behavior of impure computations differs from referentially transparent ones.