typelevel / mouse

A small companion to cats
MIT License
371 stars 66 forks source link

✨ Add `flatTapIn` and `flatTapF` to `FOptionSyntax` and `FEitherSyntax` #488

Closed jwojnowski closed 7 months ago

jwojnowski commented 7 months ago

Hi!

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

benhutchison commented 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?

jwojnowski commented 7 months ago

@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 commented 7 months ago

@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 commented 7 months ago

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 commented 7 months ago

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: