-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
attemptTap #3459
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 |
@@ -32,6 +32,9 @@ final class MonadErrorOps[F[_], E, A](private val fa: F[A]) extends AnyVal { | |||
|
|||
def redeemWith[B](recover: E => F[B], bind: A => F[B])(implicit F: MonadError[F, E]): F[B] = | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
* }}}
*/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
This is a common enough pattern that I think it deserves its own combinator. I defined it in
MonadErrorSyntax
for now, but could consider moving it toMonadError
, unsure what the criteria for that is. You could possibly write a more efficient implementation depending on yourF
and I don't think it would break compatibility either.