attemptTap#3459
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3459 +/- ##
=======================================
Coverage 91.99% 91.99%
=======================================
Files 383 383
Lines 8372 8374 +2
Branches 212 216 +4
=======================================
+ Hits 7702 7704 +2
Misses 670 670 |
| F.redeemWith[A, B](fa)(recover, bind) | ||
|
|
||
| def attemptTap[B](f: Either[E, A] => F[B])(implicit F: MonadError[F, E]): F[A] = | ||
| F.rethrow(F.flatTap(F.attempt(fa))(f)) |
There was a problem hiding this comment.
I'm not sure exactly what common pattern this is replacing? I guess exactly what the implementation is doing?
There was a problem hiding this comment.
We already have an analogue for Future#onSuccess/Future#foreach (FlatMap#flatTap) and Future#onError (ApplicativeError#onError). This would be the pure analogue to Future#onComplete, which is useful for use cases like reporting metrics or logging. This function makes call chains a bit more compact.
fa
.attempt
.flatTap {
case Right(a) => log.info(s"succeeded with $a")
case Left(e) => log.error(s"failed with $e")
}
.rethrow
// Ironically this one has fewer LOC but harder to factor out the bodies
fa
.flatTap(a => log.info(s"succeeded with $a"))
.onError(e => log.error(s"failed with $e"))
fa.attemptTap {
case Right(a) => log.info(s"succeeded with $a")
case Left(e) => log.error(s"failed with $e")
}aside: I could see an argument for accepting f as a PartialFunction
There was a problem hiding this comment.
Ah I see what you mean. Alrighty, I'm broadly 👍 on this then. Re your aside, I think the total function is better since Scala's PartialFunction/Function1 inheritance is backwards, so this is the more flexible arrangement.
travisbrown
left a comment
There was a problem hiding this comment.
I'm 👍 on the general idea, but is there a reason not to put this directly on MonadError and let the syntax be generated by simulacrum-scalafix (i.e. like the attempt, flatTap, and rethrow methods it composes)?
|
Also it'd be good to add a usage example test (like in e.g. #3467). |
|
@travisbrown Added in an example, not very happy about the |
|
@RaasAhsan, simulacrum can't handle multi-param typeclasses, that's why we have them manually defined :) |
| * scala> import cats.implicits._ | ||
| * scala> import scala.util.{Try, Success, Failure} | ||
| * | ||
| * scala> def checkError(result: Either[Throwable, Int]): Try[String] = result.fold(_ => Failure(new java.lang.Exception), _ => Success("success")) |
There was a problem hiding this comment.
just a note, but don't consider it a blocker for the PR. I personally like the example in your comment here
fa.attemptTap {
case Right(a) => log.info(s"succeeded with $a")
case Left(e) => log.error(s"failed with $e")
}It describes very well a real use case for attemptTap. A simple program could be simulated with State. Something like
/**
* {{{
* scala> import cats._, data._, implicits._
* scala> type F[A] = EitherT[State[String, *], String, A]
* scala> object log { def info(m: String): F[Unit] = EitherT.liftF(State.modify(_ => s"info: $m")); def error(m: String): F[Unit] = EitherT.liftF(State.modify(_ => s"error: $m")) }
*
* scala> val prog1: F[Int] = EitherT.liftF(State.pure(1))
* scala> val prog2: F[Int] = "this one failed".raiseError[F, Int]
*
* scala> val run1 = prog1.attemptTap { case Right(a) => log.info(a.toString); case Left(e) => log.error(e) }
* scala> val run2 = prog2.attemptTap { case Right(a) => log.info(a.toString); case Left(e) => log.error(e) }
*
* scala> run1.value.runS("init").value
* res0: String = info: 1
*
* scala> run2.value.runS("init").value
* res1: String = error: this one failed
* }}}
*/
This is a common enough pattern that I think it deserves its own combinator. I defined it in
MonadErrorSyntaxfor now, but could consider moving it toMonadError, unsure what the criteria for that is. You could possibly write a more efficient implementation depending on yourFand I don't think it would break compatibility either.