We might have a problem with monad transformers and I’m pretty sure that it isn’t addressed by the opaque type proposal, if the proposal does nothing more than what we are already doing manually.
Classic example of a monad transformer is EitherT and we’ve got its flatMap
defined to have some flexibility via an extra AA >: A
type parameter (for auto widening):
case class EitherT[F[_], A, B](value: F[Either[A, B]])
def flatMap[AA >: A, D](f: B => EitherT[F, AA, D])(implicit F: Monad[F]): EitherT[F, AA, D] = ???
}
When we implement a FlatMap[EitherT[F, E, *]]
, the implemented flatMap
doesn’t have this flexibility:
implicit def instance[F[_], E](implicit F: FlatMap[F]) =
new FlatMap[EitherT[F, E, *]] {
def flatMap[D](f: B => EitherT[F, A, D]): EitherT[F, A, D] = ???
}
We have no AA >: A
here. Meaning that when we do this:
// Import that brings the "syntax" in scope
import cats.implicits._
… we rely on the fact that the flatMap
method defined on the case class
itself has priority over the implicits that give us the flatMap syntax!
ALAS this stops being true with a type alias encoding that relies on extension methods, because those extension methods have the same priority as any other implicits and an “import cats.implicits._
”, because it is an explicit import, will no longer allow you to use the right flatMap
for EitherT
.
Or in other words, this will no longer work:
EitherT.rightT[IO, Nothing](1).flatMap { x =>
// Error! No more automatic widening for you ;-)
EitherT.right[Throwable](IO(x + 1))
}
To simplify the problem a bit, for you all to understand it, the problem we have is that this:
case class Box[A](value: A) {
def show: String =
value.toString
}
Is not equivalent with this:
opaque type Box[A] = A
object Box {
def apply[A](a: A) = a.asInstanceOf[Box[A]]
implicit class Methods[A](self: Box[A]) extends AnyVal {
def show: String = self.asInstanceOf[A].toString
}
}
Because we can have this alternative definition for that method:
object Helpers {
implicit class Methods2[A](self: Box[A]) extends AnyVal {
def show: String = "¯\_(ツ)_/¯"
}
}
And when we bring it in scope like this, then it’s going to override the opaque type’s own extension methods, something which doesn’t happen with a proper class:
import Helpers._
// Oops
Box(1).show
//=> ¯\_(ツ)_/¯
Which doesn’t seem so bad, except for the fact that the signature of the function itself can change:
object Helpers {
implicit class Methods2[A](val self: Box[A]) extends AnyVal {
def show: Int = Int.MaxValue
}
}
And now we can end up with a compile error, depending on whether that method was defined as an extension method for an opaque type or a an actual method of a proper class:
import Helpers._
val str: String = Box(1).show
^
error: type mismatch;
found : Int
required: String
This is one case in which a proper class is very clearly not behaving like a type + extension methods at compile time!
And this is very problematic for us if we want to define monad transformers, while keeping our cats.syntax
.