使用 Scala cats 编写 MTL 风格代码的给定实例的问题

Gia*_*eri 7 functional-programming scala monad-transformers scala-cats given

我正在尝试编写一些 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
Run Code Online (Sandbox Code Playgroud)

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))
Run Code Online (Sandbox Code Playgroud)

问题显然是 cats 没有定义自己的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)
Run Code Online (Sandbox Code Playgroud)

现在来看实际使用该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].
Run Code Online (Sandbox Code Playgroud)

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

  • WriterT有一个Transformer实例
  • IO有一个CanPersist实例
  • 既然WriterTaTransformerIO一个可以持久存在的monadWriterT[IO, _, _]也应该有一个CanPersist实例,有没有办法以这种方式定义所描述的Transformer类型类?编译器可以派生出这样的实例吗?在 Scala 中这是不可能的吗?

And*_*kin 3

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

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

  • -方法MonadPartialOrder要求从F到 建立一座桥梁G
  • 您的方法要求解构G[X] =>> T[M, X],然后找到一个奇特的通用桥梁构建器T,然后使用该装置从F到构建一座桥梁([X] =>> T[M, X])

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


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

解决方案与MonadPartialOrder

以下是我尝试使用MonadPartialOrderfrom解决您的问题的方法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'")


Run Code Online (Sandbox Code Playgroud)

基本思想是使用MonadPartialOrder[F, G]to get from Fto G,而不是需要 aMonadTransformer[T]来 get from Fto [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",
  )

Run Code Online (Sandbox Code Playgroud)

为什么你的方法不起作用

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

一般的 MTL 实现通常试图解决的问题有点像这样:

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

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

现在,将其与您的代码进行对比,您在代码中使用以下签名向编译器打招呼:

given monadTA[M[_]: Monad, T[_[_], _]: MonadTransformer] // ... etc
Run Code Online (Sandbox Code Playgroud)

上下文限制: 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而不是-typeclasses的原因之一MonadTransformer:它MonadPartialOrder[F, G]告诉您,对于两个固定的 monad和,您可以用G做任何事情。两个参数的类型都是,这比所有那些更高类型的 -lambda 更加良性。FFG* -> *[X[_], Y] =>> Z[X, Y]

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

For a few selected `P`, `F`, `G`, solve problem: "lift P from F to G"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^
meta-level, interpreted by humans                 easy for compiler
Run Code Online (Sandbox Code Playgroud)

而你正在尝试更接近这个的东西(挥手):

For a fixed `P`, solve: "for all `F`, `G`, lift `P` from `F` to `G`"
^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
meta-level, easy                 too hard for the compiler
Run Code Online (Sandbox Code Playgroud)

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