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

✨ Add flatTapIn and flatTapF to FOptionSyntax and FEitherSyntax #488

Conversation

jwojnowski
Copy link

Hi!

I found myself missing flatTap versions on F[Either[...]] and F[Option[...]] enough times to propose this small change 😉

@benhutchison
Copy link
Member

Thank you @jwojnowski

I'm curious.. what do you use these operators for "in the wild"? I associate flatTap with IO.println().. is that the use-case?

My concern is that the unit tests don't motivate these operations. They check contrived data but nowhere can a reader discover the intended purpose/usage of the ops, other than from the type signatures.

I also wonder if flatTapIn (without F-effect) is useful, if IO.println is the use case?

@jwojnowski
Copy link
Author

@benhutchison the use case I had in mind is subsequent validation of an F[Either[BusinessError, Result], given the validation functions Result => Either[BusinessError, Unit] (flatTapIn) or an effectful Result => F[BusinessError, Unit] (flatTapF).

Regarding the tests: true, I focused on testing the behaviour while keeping them consistent with the existing tests. I guess we can make them reflect the above use case.

A quick debugging with IO.println or effectful logging (e.g. log4cats) is certainly a use case, as is any other side effect which applies only to the Some/Right, but doesn't return anything interesting, e.g. sending an event or updating a database. While it would be possible to use combination of flatTapF and mapAsRight, a semiflatTap-like operation would be much more convenient, I think:

def semiflatTap[B](f: R => F[B]): F[Either[L, R]

@benhutchison
Copy link
Member

@jwojnowski Im still not getting you.

the flatTap family of operations perform a side effect and throw away the returned value.

therefore, the value they return is irrelevant to the program. only the effect is relevant.

So signatures with pure functions will achieve nothing, such as (& Option same)?

 def flatTapIn[A >: L, B](f: R => Either[A, B])(implicit F: Functor[F]): F[Either[A, R]] 

The flatTapF operation can be implemented once for any nested type G (Option, Either, etc) on FNested2SyntaxOps

@jwojnowski
Copy link
Author

I agree, the value is thrown away, the effect is relevant. However, we’re diverging on which effect the flatTap applies to, I think.

In my mind, the flatTapIn is essentially a counterpart to flatMapIn, so it could be represented as fEither.map(_.flatTap(f)). Here, the flatTap applies not to the F (throwing away the whole Either), but to the Either within. This way, you still throw away the result (Right part), but you can transform Right into Left.

Let’s consider an example on Either first:

import cats.implicits._

val eitherWithPositive: Either[String, Int] = Right(42)
val eitherWithNegative: Either[String, Int] = Right(-24)

def validatePositive(value: Int): Either[String, Unit] =
  Either.raiseUnless(value > 0)(s"Value must be positive, but was $value")

val positiveResult = eitherWithPositive.flatTap(validatePositive) // Right(42) // still 42 despite validatePositive returning Unit
val negativeResult = eitherWithNegative.flatTap(validatePositive) // Left(Value must be positive, but was -24)

Now, let’s do the same, but within context of F (with Try for simplicity), with flatTapIn:

type F[A] = Try[A]

val fEitherWithPositive: F[Either[String, Int]] = Try(Right(42))
val fEitherWithNegative: F[Either[String, Int]] = Try(Right(-24))

val positiveFResult = fEitherWithPositive.flatTapIn(validatePositive) // Success(Right(42)) // still 42, despite validatePositive returning Unit
val negativeFResult = fEitherWithNegative.flatTapIn(validatePositive) // Success(Left(Value must be positive, but was -24))

The same applies to Option, as it could be transformed from Some to None.

Does this example explain the idea?

@benhutchison
Copy link
Member

OK, from the description I can see there what looks (to me) a narrow set of situations where flatTapIn can make a difference.

However, I think these methods will find one user atm, namely @jwojnowski. Im not convinced they will find wider usage.

In this example use case, it seems to work around a validation of Int that instead returns an Either[String, Unit], and so to avoid the Unit clobbering the Int you flatTap instead of flatMap. I'd advocate making your validations always return Either[String, Int].

There's a clear workaround for the lack of these methods (flatMapIn).

Im going to close the PR. Criteria for re-opening:

  • Some explanation of motivation that is more general and convinces me
  • Other users mentioning they have the same problem or sending isomorphic PRs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants