2

When chaining a for comprehension with EitherT, the compiler is having issues upcasting my errors to their common ancestor type.

I have the following type definitions in my code:

sealed trait Error
object Error:
  case object Error1 extends Error
  case class Error2(someParams) extends Error

So far so good. I then had this trait where I expose this stuff:

trait SomeInterface[F[_]]:
  def someMethod(params): F[Either[Error1.type, Type1]]
  def otherMethod(params): F[Either[Error2, Type2]]

Then inside my logic I'm chaining them in this way:

for {
  t1 <- EitherT(someMethod)
  t2 <- EitherT(otherMethod)
yield t2

This should work in theory, but I'm getting the following error:

Found: EitherT[F, Error2, Type2]
Expected: EitherT[F, Error1.type, Any]

I already tried changing my interfaces so that they return the common type, but then it has trouble casting it when I implement.

I also tried passing type parameters in the EitherT constructor, but same results.

2 Answers 2

3

As @stefanobaghino said, using leftWiden[Error] fix the problem.

The root cause of the error is because Left type in Either and EitherT in cats effect is invariant and you will get type mismatch.

The article Monad transformers and cats - 3 tips for beginners might help to understand why and also show other tips about error handling.

Let’s say you want to model your business errors as a hierarchy of types extending common type CatError:

sealed abstract class CatError(msg: String)

case object CatIsHungry extends CatError("Cat is hungry!")
case object CatIsSleepy extends CatError("Cat is sleepy!")
case object CatIsIgnoringYou extends CatError("Cat is ignoring you!")
case object CatIsNotInTheMood extends CatError("Cat is not int the mood!")

Then let’s create two methods both returning nested IO and Either. The error type of first Either will be CatIsIgnoringYou and CatIsNotInTheMood for the second one. Since both error types extend CatError, you > could assume that if you fix the error type of resulting EitherT to CatError there would be no compilation errors.

import cats.data.EitherT
import cats.effect.IO

case class Cat(readyToPlay: Boolean, hasOtherPlans: Boolean, isSleepy: Boolean)

def callTheCat(cat: Cat): IO[Either[CatIsIgnoringYou.type, Cat]]
  = IO(Either.cond(!cat.hasOtherPlans, cat.copy(readyToPlay = true), CatIsIgnoringYou))

def playWithCat(cat: Cat): IO[Either[CatIsNotInTheMood.type, Unit]] =
    IO(Either.cond(!cat.isSleepy, (), CatIsNotInTheMood))

//shold compile, right?
def haveFun(cat: Cat): EitherT[IO, CatError, Unit] = for {
    readyCat <- EitherT(callTheCat(cat))
    _ <- EitherT(playWithCat(readyCat))
} yield ()

Unluckily, it’s not the case. Left type in Either and EitherT in cats effect is invariant and you will get type mismatch:

compilation error

Again fixing the problem is straightforward. You need to use function leftWiden which lets you broaden the left type of EitherT to more general CatError. It’s usually enough to use leftWiden on the last function call > in flatMap chain or for-comprehension:

import cats.data.EitherT
import cats.effect.IO
import cats.syntax.all._ // new import

// ... same code

def haveFun(cat: Cat): EitherT[IO, CatError, Unit] = for {
    readyCat <- EitherT(callTheCat(cat))
    _ <- EitherT(playWithCat(readyCat)).leftWiden[CatError] // used leftWiden to fix the issue
} yield ()
Sign up to request clarification or add additional context in comments.

Comments

2

Not a great FP expert to be honest so I'm not sure whether my solution is the "clean" one, but whenever I had this kind of problem I fixed by importing cats.syntax.bifunctor.* and calling leftWiden[Error] on the EitherT.

1 Comment

That seems to have worked. Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.