Ivan Poliakov Ivan Poliakov - 1 month ago 6
Scala Question

Implicit conversion in monadic for comprehension in Scala

Say I have the following functions:

case class ErrA(msg: String)

case class ErrB(msg: String)

def doA(): Either[ErrA, Int] = Right(2)

def doB(): Either[ErrB, Int] = Right(3)


ErrA
and
ErrB
are unrelated types and are not actually declared in the same file outside of this example. They cannot be easily made to inherit from a common type.

I would like to introduce a new type that would represent both error types:

sealed trait ErrAOrB

case class ErrAWrapper(e: ErrA) extends ErrAOrB

case class ErrBWrapper(e: ErrB) extends ErrAOrB


And then write the following function using a for comprehension:

def doAplusB(): Either[ErrAOrB, Int] =
for (a <- doA().right;
b <- doB().right) yield a + b


Is there any way to get the compiler to implicitly convert those specific
Either
types to the common
Either[ErrAOrB, Int]
type?

For instance:

implicit def wrapA[T](res: Either[ErrA, T]): Either[ErrAOrB, T] = res.left.map(ErrAWrapper(_))

implicit def wrapB[T](res: Either[ErrB, T]): Either[ErrAOrB, T] = res.left.map(ErrBWrapper(_))


But that doesn't work because the implicit conversion gets applied only to the final expression in the for comprehension, and then the compiler has to bind it to
doA
and since types
ErrA
and
ErrAOrB
are unrelated the best it can do is to make the generics make sense is to use
Object
which is not compatible with the expected type.

Answer

Implicit views are not recommended in Scala, as you can see from compiler's feature warning when you define wrapA/wrapB.

Even if you take your chances by defining implicit view over Either.RightProjection instead Either - Just imagine people reading your code and wondering about how did Either[ErrA, Int] become Either[ErrAOrB, Int]? No IDE hint, so no good way to find your wrapA implicits

So, use implicit class instead:

implicit class WrapA[T](x: Either[ErrA, T]) {
  def wrap = x.left.map(ErrAWrapper(_)): Either[ErrAOrB, T]
}

implicit class WrapB[T](x: Either[ErrB, T]) {
  def wrap = x.left.map(ErrBWrapper(_)): Either[ErrAOrB, T]
}

scala> def doAplusB(): Either[ErrAOrB, Int] =
     |   for {
     |      a <- doA().wrap.right
     |      b <- doB().wrap.right
     |   } yield a + b
doAplusB: ()Either[ErrAOrB,Int]

P.S. You don't need monad for + operation if your computations are independent (like in your example) - Applicative is enough. Take a look at cats.data.Validated or scalaz.Validation for instance.


Answering wether it's possible or not to trick scalac:

implicit def wrapBB[T](res: Either.RightProjection[ErrB, T]): Either.RightProjection[ErrAOrB, T] = res.e.left.map(ErrBWrapper(_)).right

implicit def wrapAA[T](res: Either.RightProjection[ErrA, T]): Either.RightProjection[ErrAOrB, T] = res.e.left.map(ErrAWrapper(_)).right

def doAplusB(): Either[ErrAOrB, Int] =
  for (a <- doA().right: Either.RightProjection[ErrAOrB, Int] ;
       b <- doB().right: Either.RightProjection[ErrAOrB, Int]) yield a + b

But this requires Either.RightProjection type ascription, but if you had some parallel assignment (Applicative-style instead of for-comprehension), I believe the thing with ad-hoc supertype could work.

Or even (with your wrapB defined):

implicit def wrapAA[T] ...
implicit def wrapBB[T] ...

implicit def wrapB[T](res: Either[ErrB, T]): Either[ErrAOrB, T] = res.left.map(ErrBWrapper(_))

def doAplusB(): Either[ErrAOrB, Int] =
  for (a <- doA().right:  Either.RightProjection[ErrAOrB, Int];
       b <- doB().right) yield a + b

The reason is an expansion to:

doA().right.flatMap(a => doB().right.map(b => a + b))

flatMap requires RightProjection to be returned, but map doesn't.