[英]Problem with given instances writing MTL style code with Scala cats
I am trying to write some Scala code to have custom behaviour in an mtl style .我正在尝试编写一些 Scala 代码以具有mtl 样式的自定义行为。 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: IO 实例可以轻松实现,但我感兴趣的是自动为任何基于 IO 的 monad 堆栈提供实例:
// 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;问题显然是猫没有定义自己的MonadTransformer
类型类; 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:现在进入实际使用CanPersist
功能的代码:
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:我认为编译器将能够导出正确的实例:
WriterT
has a Transformer
instance WriterT
有一个Transformer
实例IO
has a CanPersist
instance IO
有一个CanPersist
实例WriterT
is a Transformer
and IO
a monad that can persist WriterT[IO, _, _]
should also have a CanPersist
instance Is there a way to define the described Transformer
typeclass this way?由于WriterT
是一个Transformer
和IO
一个可以持久WriterT[IO, _, _]
的 monad 也应该有一个CanPersist
实例有没有办法以这种方式定义描述的Transformer
类型类? Can the compiler derive such instances or is it impossible in Scala?编译器可以派生这样的实例还是在 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.推理问题似乎是您链接的特定 MTL 实现依赖于MonadPartialOrder等特征而不是MonadTransformer
-typeclasses 的原因之一。
Basically, what happens here is this: When you want to get from F
to G
基本上,这里发生的是:当你想从F
到G
MonadPartialOrder
-approach asks for a bridge from F
to G
MonadPartialOrder
- 方法要求从F
到G
的桥梁G
into [X] =>> T[M, X]
, then find a fancy universal bridge-builder T
, and then use that contraption to build a bridge from F
to ([X] =>> T[M, X])
.您的方法要求将G
解构为[X] =>> T[M, X]
,然后找到一个花哨的通用桥梁建造者T
,然后使用该装置建造从F
到([X] =>> T[M, X])
的桥梁([X] =>> T[M, X])
。 Thus, cats.mtl
's approach is much simpler, and far less demanding of the inference algorithm.因此, cats.mtl
的方法要简单得多,并且对推理算法的要求要低得多。 That's why cats.mtl
works, whereas your approach doesn't.这就是为什么cats.mtl
有效,而您的方法无效。
I'll first sketch how your example can be fixed, then I'll speculate a little about why your approach does not work.我将首先概述如何修复您的示例,然后我将推测一下您的方法为何不起作用。
MonadPartialOrder
MonadPartialOrder
的解决方案Here is how I'd try to approach your problem using the MonadPartialOrder
from cats.mtl
:以下是我尝试使用MonadPartialOrder
中的cats.mtl
解决您的问题的方法:
import cats.data.WriterT
import cats.syntax.all._
import cats.mtl.MonadPartialOrder
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
given persistTA[F[_]: CanPersist: Monad, G[_]]
(using mpo: MonadPartialOrder[F, G]): CanPersist[G] with
def persistToDB[A](a: A): G[Unit] =
mpo(summon[CanPersist[F]].persistToDB(a))
def saveIntString[M[_]: Monad]
(int: Int, string: String)
(using P:CanPersist[M])
: M[String] =
for {
_ <- P.persistToDB(int)
_ <- P.persistToDB(string)
} yield "done"
def res: WriterT[IO, String, String] = saveIntString(2, "test")
@main def main(): Unit =
println("Run it with 'sbt clean compile run'")
The basic idea is to use MonadPartialOrder[F, G]
to get from F
to G
, instead of requiring a MonadTransformer[T]
to get from F
to [X] =>> T[F, X]
.基本思想是使用MonadPartialOrder[F, G]
从F
到G
,而不是要求MonadTransformer[T]
从F
到[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:这在 Scala 3.1.2 上编译和运行得很好,如果你想尝试一下,这里有一个完整的build.sbt
:
import Dependencies._
ThisBuild / scalaVersion := "3.1.2"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "com.foobarbaz"
ThisBuild / organizationName := "example"
lazy val root = (project in file("."))
.settings(
name := "cats-mtl-so-question-72407103",
scalacOptions += "-explaintypes",
libraryDependencies += scalaTest % Test,
libraryDependencies += "org.typelevel" %% "cats-core" % "2.7.0",
libraryDependencies += "org.typelevel" %% "cats-mtl" % "1.2.1",
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.4-389-3862cf0",
)
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.mtl
does), is that your solution is attempting to work at a higher level of abstraction than cats.mtl
does.您的解决方案不起作用(而cats.mtl
)的原因是您的解决方案试图在比cats.mtl
更高的抽象级别上工作。
The problem that an average MTL implementation is usually trying to solve looks somewhat like this:一个普通的 MTL 实现通常试图解决的问题看起来有点像这样:
For a fixed property
P
and two fixed monadsLameMonad
andFancyMonad
, find a way to liftP
from theLameMonad
to theFancyMonad
.对于一个固定属性P
和两个固定单子LameMonad
和FancyMonad
,找到一种方法将P
从LameMonad
提升到FancyMonad
。
This is done for a few useful properties P
(such as that you can Ask
, Tell
, access and mutate Stateful
stuff and so on), and a reasonable amount of different combinations of LameMonad
and FancyMonad
, with the fancy monads usually arising from the lame monads by applying some monad transformer (such as those from cats.data._
).这是为一些有用的属性P
完成的(例如,您可以Ask
、 Tell
、访问和改变Stateful
的东西等等),以及LameMonad
和FancyMonad
的合理数量的不同组合,花式 monad 通常来自 lame通过应用一些 monad 转换器(例如来自于cats.data._
的那些)来创建 monads。 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.注意量词“for a few” 、 “for a proper amount”是如何出现在我们试图自动解决的问题陈述之外的元讨论中的。
Now, contrast this to your code, where you greet the compiler with the following signature:现在,将此与您的代码进行对比,您在代码中使用以下签名向编译器致意:
given monadTA[M[_]: Monad, T[_[_], _]: MonadTransformer] // ... etc
The contextual bound : MonadTransformer
demands that the compiler solves a problem that looks roughly like上下文绑定: MonadTransformer
要求编译器解决一个看起来大致类似的问题
For a fixed
T
, find a unique constructive proof that for all monadsM
,[X] => T[M, X]
is also a monad.对于固定的T
,找到一个唯一的构造证明,证明对于所有单子M
,[X] => T[M, X]
也是单子。
Note how the for all
quantifier 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 kind Foo
against [A] =>> T[M, A]
with a higher-kinded M
.请注意for all
量词现在如何滑入我们试图自动化的任务的问题陈述中,并且还要注意现在编译器应该以某种方式推断“正确”的方式来匹配更高种类的Foo
与[A] =>> T[M, A]
具有更高种类的M
。
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.匹配[A] =>> T[M, A]
的任务很棘手(由于子类化/继承甚至比在 Haskell 中更棘手),而且实际上有些不明确。 For example, WriterT[IO, String, V]
can be decomposed in multiple ways: is it例如WriterT[IO, String, V]
可以有多种分解方式:是
[X[_], Y] =>> WriterT[X, String, Y]
applied toIO
andV
[X[_], Y] =>> WriterT[X, String, Y]
应用于IO
和V
or is it或者是
[X[_], Y] =>> WriterT[IO, Y, X[V]]
applied toId[_]
andString
[X[_], Y] =>> WriterT[IO, Y, X[V]]
应用于Id[_]
和String
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.mtl
is using MonadPartialOrder
instead of MonadTransformer
-typeclasses: the MonadPartialOrder[F, G]
tells you just that you can do with G
anything you can do with F
, for two fixed monads F
and G
.我还假设这是cats.mtl
使用MonadPartialOrder
而不是MonadTransformer
-typeclasses 的原因之一: MonadPartialOrder[F, G]
告诉你你可以用G
做任何你可以用F
做的事情,对于两个固定的单子F
和G
。 The kinds of both parameters are * -> *
, which is much more benign than all those higher-kinded [X[_], Y] =>> Z[X, Y]
-lambdas.这两个参数的种类都是* -> *
,这比所有那些更高种类的[X[_], Y] =>> Z[X, Y]
-lambdas 更加良性。
So, to reiterate, MTL is doing this:因此,重申一下,MTL 正在这样做:
For a few selected `P`, `F`, `G`, solve problem: "lift P from F to G"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
meta-level, interpreted by humans easy for compiler
whereas you are attempting something closer to this ( waves hands handwavily ):而您正在尝试更接近此的东西(挥手挥手):
For a fixed `P`, solve: "for all `F`, `G`, lift `P` from `F` to `G`"
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
meta-level, easy too hard for the compiler
which is sufficient, but not necessary (and therefore unnecessarily hard for the compiler).这是足够的,但不是必需的(因此对编译器来说是不必要的困难)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.