繁体   English   中英

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

[英]Problem with given instances writing MTL style code with Scala cats

我正在尝试编写一些 Scala 代码以具有mtl 样式的自定义行为。 例如,为了公开对特定效果进行抽象的“写入数据库”功能,我编写了自己的类型类:

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

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))

问题显然是猫没有定义自己的MonadTransformer类型类; 幸运的是,编写自己的代码非常简单:

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)

现在进入实际使用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].

问题是编译器显然无法导出正确的实例; 这让我很困惑。 我认为编译器将能够导出正确的实例:

  • WriterT有一个Transformer实例
  • IO有一个CanPersist实例
  • 由于WriterT是一个TransformerIO一个可以持久WriterT[IO, _, _]的 monad 也应该有一个CanPersist实例有没有办法以这种方式定义描述的Transformer类型类? 编译器可以派生这样的实例还是在 Scala 中是不可能的?

推理问题似乎是您链接的特定 MTL 实现依赖于MonadPartialOrder等特征而不是MonadTransformer -typeclasses 的原因之一。

基本上,这里发生的是:当你想从FG

  • MonadPartialOrder - 方法要求从FG的桥梁
  • 您的方法要求将G解构为[X] =>> T[M, X] ,然后找到一个花哨的通用桥梁建造者T ,然后使用该装置建造从F​​到([X] =>> T[M, X])的桥梁([X] =>> T[M, X])

因此, cats.mtl的方法要简单得多,并且对推理算法的要求要低得多。 这就是为什么cats.mtl有效,而您的方法无效。


我将首先概述如何修复您的示例,然后我将推测一下您的方法为何不起作用。

MonadPartialOrder的解决方案

以下是我尝试使用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'")


基本思想是使用MonadPartialOrder[F, G]FG ,而不是要求MonadTransformer[T]F[X] =>> T[F, X]

这在 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",
  )

为什么你的方法不起作用

您解释中的逻辑对我来说似乎很好,所以我想说编译器目前无法推断出所需的类型类。 您的解决方案不起作用(而cats.mtl )的原因是您的解决方案试图在比cats.mtl更高的抽象级别上工作。

一个普通的 MTL 实现通常试图解决的问题看起来有点像这样:

对于一个固定属性P和两个固定单子LameMonadFancyMonad ,找到一种方法将PLameMonad提升到FancyMonad

这是为一些有用的属性P完成的(例如,您可以AskTell 、访问和改变Stateful的东西等等),以及LameMonadFancyMonad的合理数量的不同组合,花式 monad 通常来自 lame通过应用一些 monad 转换器(例如来自于cats.data._的那些)来创建 monads。 注意量词“for a few”“for a proper amount”是如何出现在我们试图自动解决的问题陈述之外的元讨论中的。

现在,将此与您的代码进行对比,您在代码中使用以下签名向编译器致意:

given monadTA[M[_]: Monad, T[_[_], _]: MonadTransformer] // ... etc

上下文绑定: MonadTransformer要求编译器解决一个看起来大致类似的问题

对于固定的T ,找到一个唯一的构造证明,证明对于所有单子M[X] => T[M, X]也是单子。

请注意for all量词现在如何滑入我们试图自动化的任务的问题陈述中,并且还要注意现在编译器应该以某种方式推断“正确”的方式来匹配更高种类的Foo[A] =>> T[M, A]具有更高种类的M

匹配[A] =>> T[M, A]的任务很棘手(由于子类化/继承甚至比在 Haskell 中更棘手),而且实际上有些不明确。 例如WriterT[IO, String, V]可以有多种分解方式:是

[X[_], Y] =>> WriterT[X, String, Y]应用于IOV

或者是

[X[_], Y] =>> WriterT[IO, Y, X[V]]应用于Id[_]String

还是任何其他组合? 一些约定(首先采用最右边的参数等)似乎在最常见的情况下有效,但显然不适用于您的特定情况。

因此,在无法确定的情况下,我假设所有那些对更高种类的通用量化以某种方式设法使编译器严重混淆,以至于该方法变得不切实际。 我还假设这是cats.mtl使用MonadPartialOrder而不是MonadTransformer -typeclasses 的原因之一: MonadPartialOrder[F, G]告诉你你可以用G做任何你可以用F做的事情,对于两个固定的单子FG 这两个参数的种类都是* -> * ,这比所有那些更高种类的[X[_], Y] =>> Z[X, Y] -lambdas 更加良性。

因此,重申一下,MTL 正在这样做:

For a few selected `P`, `F`, `G`, solve problem: "lift P from F to G"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^
meta-level, interpreted by humans                 easy for compiler

而您正在尝试更接近此的东西(挥手挥手):

For a fixed `P`, solve: "for all `F`, `G`, lift `P` from `F` to `G`"
^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
meta-level, easy                 too hard for the compiler

这是足够的,但不是必需的(因此对编译器来说是不必要的困难)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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