cats flatTap or scala tap

Viewed 882

I have a hardtime understanding what cats flatTap is for ?

def flatTap[B](f: A => F[B]): F[A] = typeClassInstance.flatTap[A, B](self)(f)

Actually i was just trying to tap into the value of an effect for logging and then returning that effect.

It seems that scala 2.13 tap is the way to go. However with an effect like Either we need to test things:

(Right(2).withLeft[Throwable]).tap(....)
def tap[U](f: A => U): A = {
    f(self)
    self
  }

any suggestion on how i can achieve this without being too sophisticated such as introducing advanced lib and such ....

1 Answers

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.

Related