简体   繁体   English

给定实例使用 Scala 猫编写 MTL 样式代码的问题

[英]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实例
  • Since 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是一个TransformerIO一个可以持久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基本上,这里发生的是:当你想从FG

  • The MonadPartialOrder -approach asks for a bridge from F to G MonadPartialOrder - 方法要求从FG的桥梁
  • Your approach asks to deconstruct 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.我将首先概述如何修复您的示例,然后我将推测一下您的方法为何不起作用。

A solution with 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]FG ,而不是要求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",
  )

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.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 monads LameMonad and FancyMonad , find a way to lift P from the LameMonad to the FancyMonad .对于一个固定属性P和两个固定单子LameMonadFancyMonad ,找到一种方法将PLameMonad提升到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完成的(例如,您可以AskTell 、访问和改变Stateful的东西等等),以及LameMonadFancyMonad的合理数量的不同组合,花式 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 monads M , [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 to IO and V [X[_], Y] =>> WriterT[X, String, Y]应用于IOV

or is it或者是

[X[_], Y] =>> WriterT[IO, Y, X[V]] applied to Id[_] and String [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做的事情,对于两个固定的单子FG 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM