Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using EitherT to evaluate the results of operations using a shared error type inheritance?

I have an error type hierarchy for peeling bananas:

sealed trait PeelBananaError
object PeelBananaError {
  case object TooRipe extends PeelBananaError
  case object NotRipeEnough extends PeelBananaError
}

And I have some results in EitherT that we know can only fail in one of the two ways:

val peelBrownBananaResult: EitherT[Future, TooRipe, String] = ...
val peelGreenBananaResult: EitherT[Future, NotRipeEnough, String] = ...

Now I need to gather the String results from the right and combine them for a final result:

val combinedResult: EitherT[Future, PeelBananaError, String] = for {
  first <- peelBrownBananaResult
  second <- peelGreenBananaResult
} yield (first + second)

But trying this out gives me a compile error:

cmd15.sc:2: inferred type arguments [PeelBananaError.NotRipeEnough.type,String] do not conform to method flatMap's type parameter bounds [AA >: PeelBananaError.TooRipe.type,D]
  first <- peelBrownBananaResult
           ^
cmd15.sc:2: type mismatch;
 found   : String => cats.data.EitherT[scala.concurrent.Future,PeelBananaError.NotRipeEnough.type,String]
 required: String => cats.data.EitherT[scala.concurrent.Future,AA,D]
  first <- peelBrownBananaResult
        ^
Compilation Failed

It seems the compiler is unable to infer that the Left types share a common inheritance in PeelBananaError so the for comprension does not compile. Is there any workaround here? Some hint I can give to the compiler so I can run this for comprehension?

I have tried the following, but using .asInstanceOf seems so hacky and the resulting code looks very ugly, is this really the only solution?

val combinedResult: EitherT[Future, PeelBananaError, String] = for {
  first <- peelBrownBananaResult.asInstanceOf[EitherT[Future,PeelBananaError,String]]
  second <- peelGreenBananaResult.asInstanceOf[EitherT[Future,PeelBananaError,String]]
} yield (first + second)
like image 330
Cory Klein Avatar asked May 21 '21 18:05

Cory Klein


Video Answer


1 Answers

EitherT monad transformer is not covariant in A or B type parameters

case class EitherT[F[_], A, B]

unlike Either which is covariant in A and B

sealed abstract class Either[+A, +B]

which is why Either type-checks to a supertype

Either.left(TooRipe): Either[PeelBananaError, String] // ok

but EitherT does not

EitherT.left(Future(TooRipe)): EitherT[Future, PeelBananaError, String] // error

However we can safely widen monad transformers

for {
  first <- peelBrownBananaResult.leftWiden[PeelBananaError]
  second <- peelGreenBananaResult.leftWiden[PeelBananaError]
} yield (first + second)
// : EitherT[Future, PeelBananaError, String] = ...

unlike unsafe widening with asInstanceOf

type T = Either[TooRipe.type, String]
val a = Either.left[PeelBananaError, String](new PeelBananaError {})
val b: Either[TooRipe.type, String] = a.asInstanceOf[T] // run-time error
val c: Either[TooRipe.type, String] = a.widen[T]        // compile-time error
like image 197
Mario Galic Avatar answered Oct 26 '22 01:10

Mario Galic