Closed jwojnowski closed 7 months ago
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?
@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]
@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
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?
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:
Hi!
I found myself missing
flatTap
versions onF[Either[...]]
andF[Option[...]]
enough times to propose this small change 😉