I am trying to write some Scala code to have custom behaviour in an mtl style. For example, in order to expose the "write to DB" functionality abstracting over the specific effect I wrote my own type class:
trait CanPersist[M[_]]:
def persistToDB[A](a: A): M[Unit]
given CanPersist[IO] with
def persistToDB[A](a: A): IO[Unit] = IO(???) // Write to DB
The IO instance can be easily implemented but what I'm interested in is automatically providing the instance for any IO-based monad stack:
// If a Transformer wraps a Monad that can persist then it can persist too
given persistTA[M[_]: CanPersist: Monad, T[_[_], _]: MonadTransformer]:
CanPersist[[A] =>> T[M, A]] with
def persistToDB[A](a: A): T[M, Unit] =
summon[MonadTransformer[T]].lift(summon[CanPersist[M]].persistToDB(a))
The problem is apparently cats does not define its own MonadTransformer type class; luckily its pretty straightforward to write your own:
trait MonadTransformer[T[_[_], _]]:
def lift[M[_]: Monad, A](ma: M[A]): T[M, A]
// A Monad Transformer is a Monad if it wraps a Monad
given monadTA[M[_]: Monad, T[_[_], _]: MonadTransformer]: Monad[[A] =>> T[M, A]] with
def pure[A](a: A): T[M, A] = ??? // implementations are not relevant
def flatMap[A, B](fa: T[M, A])(f: A => T[M, B]): T[M, B] = ???
def tailRecM[A, B](a: A)(f: A => T[M, Either[A, B]]): T[M, B] = ???
// Both WriterT and EitherT are Monad Transformers
given writerMT[L: Monoid]: MonadTransformer[[M[_], A] =>> WriterT[M, L, A]] with
def lift[M[_]: Monad, A](ma: M[A]): WriterT[M, L, A] =
WriterT.liftF(ma)
given eitherMT[Err]: MonadTransformer[[M[_], A] =>> EitherT[M, Err, A]] with
def lift[M[_]: Monad, A](ma: M[A]): EitherT[M, Err, A] =
EitherT.liftF(ma)
And now onto the code that actually uses the CanPersist functionality:
def saveIntString[M[_]: Monad]
(int: Int, string: String)
(using P:CanPersist[M])
: M[String] =
for {
_ <- P.persistToDB(int)
_ <- P.persistToDB(string)
} yield "done"
val res: WriterT[IO, String, String] = saveIntString(2, "test")
// Does not compile:
// no implicit argument of type CanPersist[M] was found for parameter P of method saveIntString
// where: M is a type variable with constraint <: [V] =>> cats.data.WriterT[cats.effect.IO, String, V]
// I found:
// persistTA[M, T]
// But given instance persistTA does not match type CanPersist[M].
The problem is the compiler apparently can not derive the correct instances; this confuses me though. I thought the compiler would be able to derive the correct instance:
WriterThas aTransformerinstanceIOhas aCanPersistinstance- Since
WriterTis aTransformerandIOa monad that can persistWriterT[IO, _, _]should also have aCanPersistinstance Is there a way to define the describedTransformertypeclass this way? Can the compiler derive such instances or is it impossible in Scala?
Problems with inference seem to be one of the reasons why the particular MTL implementation that you linked is relying on traits such as MonadPartialOrder instead of
MonadTransformer-typeclasses.Basically, what happens here is this: When you want to get from
FtoGMonadPartialOrder-approach asks for a bridge fromFtoGGinto[X] =>> T[M, X], then find a fancy universal bridge-builderT, and then use that contraption to build a bridge fromFto([X] =>> T[M, X]).Thus,
cats.mtl's approach is much simpler, and far less demanding of the inference algorithm. That's whycats.mtlworks, whereas your approach doesn't.I'll first sketch how your example can be fixed, then I'll speculate a little about why your approach does not work.
A solution with
MonadPartialOrderHere is how I'd try to approach your problem using the
MonadPartialOrderfromcats.mtl:The basic idea is to use
MonadPartialOrder[F, G]to get fromFtoG, instead of requiring aMonadTransformer[T]to get fromFto[X] =>> T[F, X].This compiles and runs just fine on Scala 3.1.2, here is a complete
build.sbt, if you want to try it out:Why your approach does not work
The logic in your explanation seems fine to me, so I would say that the compiler currently cannot infer the required typeclasses. The reason why your solution does not work (whereas
cats.mtldoes), is that your solution is attempting to work at a higher level of abstraction thancats.mtldoes.The problem that an average MTL implementation is usually trying to solve looks somewhat like this:
This is done for a few useful properties
P(such as that you canAsk,Tell, access and mutateStatefulstuff and so on), and a reasonable amount of different combinations ofLameMonadandFancyMonad, with the fancy monads usually arising from the lame monads by applying some monad transformer (such as those fromcats.data._). Note how the the quantifiers "for a few", "for a reasonable amount" appear in the metadiscussion outside of the problem statement that we're trying to solve automatically.Now, contrast this to your code, where you greet the compiler with the following signature:
The contextual bound
: MonadTransformerdemands that the compiler solves a problem that looks roughly likeNote how the
for allquantifier has now slipped into the problem statement of the task we are trying to automate, and also note that now the compiler is somehow supposed to infer the "right" way to match a higher kindFooagainst[A] =>> T[M, A]with a higher-kindedM.The task of matching against
[A] =>> T[M, A]is tricky (thanks to subclassing / inheritance even trickier than in Haskell), and actually somewhat ill-defined. For example,WriterT[IO, String, V]can be decomposed in multiple ways: is itor is it
or is it any other combination? Some conventions (taking the rightmost argument first etc.) seem to work in most common cases, but apparently not in your particular case.
So, without being able to tell for sure, I assume that all those universal quantifications over higher kinds somehow manage to confuse the compiler badly enough that the approach becomes impractical. I also assume that this is one of the reasons why
cats.mtlis usingMonadPartialOrderinstead ofMonadTransformer-typeclasses: theMonadPartialOrder[F, G]tells you just that you can do withGanything you can do withF, for two fixed monadsFandG. The kinds of both parameters are* -> *, which is much more benign than all those higher-kinded[X[_], Y] =>> Z[X, Y]-lambdas.So, to reiterate, MTL is doing this:
whereas you are attempting something closer to this (waves hands handwavily):
which is sufficient, but not necessary (and therefore unnecessarily hard for the compiler).