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
实例WriterT
aTransformer
和IO
一个可以持久存在的monadWriterT[IO, _, _]
也应该有一个CanPersist
实例,有没有办法以这种方式定义所描述的Transformer
类型类?编译器可以派生出这样的实例吗?在 Scala 中这是不可能的吗?推理问题似乎是您链接的特定 MTL 实现依赖于MonadPartialOrder等特征而不是-typeclasses的原因之一MonadTransformer
。
基本上,这里发生的事情是这样的:当你想要从F
到G
MonadPartialOrder
要求从F
到 建立一座桥梁G
G
为[X] =>> T[M, X]
,然后找到一个奇特的通用桥梁构建器T
,然后使用该装置从F
到构建一座桥梁([X] =>> T[M, X])
。因此,cats.mtl
的方法要简单得多,并且对推理算法的要求要低得多。这就是为什么cats.mtl
有效,而你的方法却行不通。
我将首先概述如何修复您的示例,然后我将推测一下为什么您的方法不起作用。
MonadPartialOrder
以下是我尝试使用MonadPartialOrder
from解决您的问题的方法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 F
to G
,而不是需要 aMonadTransformer[T]
来 get from F
to [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
和两个固定的单子LameMonad
和FancyMonad
,找到一种从 提升P
到LameMonad
the 的方法FancyMonad
。
这样做是为了一些有用的属性P
(例如,您可以Ask
、Tell
、 访问和改变Stateful
东西等等),以及 和的合理数量的不同组合,LameMonad
而FancyMonad
花哨的 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]
应用于IO
和V
或者是
[X[_], Y] =>> WriterT[IO, Y, X[V]]
应用于Id[_]
和String
或者是其他组合吗?一些约定(首先采用最右边的参数等)似乎适用于大多数常见情况,但显然不适用于您的特定情况。
因此,在无法确定的情况下,我假设所有这些对更高类型的通用量化都以某种方式使编译器严重困惑,以至于该方法变得不切实际。cats.mtl
我还假设这是使用MonadPartialOrder
而不是-typeclasses的原因之一MonadTransformer
:它MonadPartialOrder[F, G]
告诉您,对于两个固定的 monad和,您可以用G
做任何事情。两个参数的类型都是,这比所有那些更高类型的 -lambda 更加良性。F
F
G
* -> *
[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)
这是足够的,但不是必需的(因此对编译器来说不必要的困难)。