Skip to content
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

Merged
merged 5 commits into from
Jul 5, 2020
Merged

attemptTap #3459

merged 5 commits into from
Jul 5, 2020

Conversation

RaasAhsan
Copy link

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 to MonadError, unsure what the criteria for that is. You could possibly write a more efficient implementation depending on your F and I don't think it would break compatibility either.

@codecov-commenter
Copy link

codecov-commenter commented Jun 7, 2020

Codecov Report

Merging #3459 into master will increase coverage by 0.00%.
The diff coverage is 100.00%.

@@           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))
Copy link
Member

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?

Copy link
Author

@RaasAhsan RaasAhsan Jun 9, 2020

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

Copy link
Member

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.

djspiewak
djspiewak previously approved these changes Jun 10, 2020
Copy link
Contributor

@travisbrown travisbrown left a 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)?

@travisbrown
Copy link
Contributor

Also it'd be good to add a usage example test (like in e.g. #3467).

@travisbrown travisbrown added this to the 2.2.0-M3 milestone Jun 12, 2020
@RaasAhsan
Copy link
Author

@travisbrown MonadError and ApplicativeError don't seem to be annotated with @typeclass, so no syntax is generated, which I'm guessing is why they're all manually defined in the cats.syntax.monadError package. Should they be?

Added in an example, not very happy about the println but it demonstrates that some effect happens

@LukaJCB
Copy link
Member

LukaJCB commented Jun 16, 2020

@RaasAhsan, simulacrum can't handle multi-param typeclasses, that's why we have them manually defined :)

LukaJCB
LukaJCB previously approved these changes Jun 16, 2020
* 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"))
Copy link
Contributor

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
 * }}}
 */

@travisbrown travisbrown self-requested a review July 5, 2020 06:26
Copy link
Contributor

@travisbrown travisbrown left a 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!

@travisbrown travisbrown modified the milestones: 2.2.0, 2.2.0-RC1 Jul 5, 2020
@travisbrown travisbrown merged commit b74f104 into typelevel:master Jul 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants