Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala cats, flatMap on cartesian?

Given:

import cats.syntax.cartesian._

type M[X] = Future[Either[Error, X]]
val ma: M[A] = ???
val mb: M[B] = ???

I know I can do that:

def doStuff(a: A, b: B): C = ???
val result: M[C] = (ma |@| mb).map(doStuff)

But how do I flatMap? There's no flatMap in the CartesianBuilders.

def doFancyStuff(a: A, b: B): M[C] = ???
val result: M[C] = (ma |@| mb).flatMap(doFancyStuff)
like image 487
Victor Basso Avatar asked Apr 21 '26 02:04

Victor Basso


1 Answers

I think the main problem is that it's awkward to flatMap on a Future[Either[Error, X]] in the sense of EitherT[Future, Error, X]-monad stack, because the original flatMap of the Future gets in the way, and the compiler isn't looking for a monad instance that could handle the combination of Future and Either simultaneously. However, if you wrap the futures in EitherT, everything works smoothly.

For cats 1.0.1

In the following, (a,b).tupled corresponds to Cartesian.product(a, b), and (a, b).mapN corresponds to the deprecated (a |@| b).map.

Given types A, B, C, Error, the following code snippets compile under cats 1.0.1:

If you want to preserve your definition of M (you probably should), then you can avoid the above mentioned problems by wrapping everything in EitherT and then extracting the value:

import scala.util.Either
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global // required by Monad[Future]
import cats.instances.future._                            // Monad for `Future`
import cats.syntax.apply._                                // `tupled` and `mapN`
import cats.data.EitherT                                  // EitherT monad transformer

type M[X] = Future[Either[Error, X]]
val ma: M[A] = ???
val mb: M[B] = ???

def doStuff(a: A, b: B): C = ???
val result1: M[C] = (EitherT(ma), EitherT(mb)).mapN(doStuff).value

def doFancyStuff(a: A, b: B): M[C] = ???
val result2: M[C] = (for {
  ab <- (EitherT(ma), EitherT(mb)).tupled
  c <- EitherT(doFancyStuff(ab._1, ab._2))
} yield c).value

However, if this seems too awkward, and you can adjust the definition of M, the following variant could be slightly shorter:

import scala.util.Either
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global // required by Monad[Future]
import cats.instances.future._                            // Monad for `Future`
import cats.syntax.apply._                                // `tupled` and `mapN`
import cats.data.EitherT                                  // EitherT monad transformer

type M[X] = EitherT[Future, Error, X]
val ma: M[A] = ??? // either adjust signatures, or wrap result in EitherT(res)
val mb: M[B] = ???

def doStuff(a: A, b: B): C = ???
val result1: M[C] = (ma, mb).mapN(doStuff)

def doFancyStuff(a: A, b: B): M[C] = ???
val result2: M[C] = (ma, mb).tupled.flatMap{
  case (a, b) => doFancyStuff(a, b)
}

This is because (ma, mb).tupled builds a M[(A, B)], where M is the previously mentioned monad stack, which then can be easily flatMapped with a (A, B) => M[C] function to M[C].

For older versions with Cartesian (untested)

Assuming that (ma, mb).tupled corresponds to the deprecated Cartesian.product and (ma, mb).mapN corresponds to the deprecated (ma |@| mb).map, the two definitions of result1 and result2 in the above code snippet for 1.0.1 translate to:

val result1: M[C] = (ma |@| mb).map(doStuff)
val result2: M[C] = Cartesian[M].product(ma, mb).flatMap{ 
  case (a, b) => doFancyStuff(a, b) 
}

Again, this works only because Cartesian[M].product(ma, mb) builds an M[(A, B)] from M[A] and M[B], where M[X] is defined as EitherT[Future, Error, X]. If it were defined as Future[Either[Error, X]], then the flatMap would be invoked on the Future, and instead of doFancyStuff we would have to pass something like Either[Error, (A, B)] => Future[Either[Error, C]], which is probably not what you want.

like image 139
Andrey Tyukin Avatar answered Apr 30 '26 19:04

Andrey Tyukin