用于 IO monad 的复杂 monad 转换器

Posted

技术标签:

【中文标题】用于 IO monad 的复杂 monad 转换器【英文标题】:Complex monad transformer for IO monad 【发布时间】:2021-11-27 06:17:29 【问题描述】:

我正在尝试编写将实体保存到数据库的函数的Cats MTL 版本。我希望这个函数从环境中读取一些SaveOperation[F[_]],执行它并处理可能的失败。到目前为止,我想出了这个函数的 2 个版本:save 是更多态的 MTL 版本,save2 在其签名中使用精确的单子,这意味着我将自己限制在使用 IO

  type SaveOperation[F[_]] = Employee => F[Int]

  def save[F[_] : Monad](employee: Employee)(implicit
                                     A: Ask[F, SaveOperation[F]],
                                     R: Raise[F, AppError]): F[Unit] =
    for 
      s <- A.ask
      rows <- s(employee)
      res <- if rows != 1 then R.raise(FailedInsertion)
             else ().pure[F]
     yield res

  def save2(employee: Employee): Kleisli[IO, SaveOperation[IO], Either[AppError, Unit]] =
    Kleisli((saveOperation) => saveOperation(employee)
      .handleErrorWith(err => IO.pure(Left(PersistenceError(err))))
      .map(rows =>
        if rows != 1 then Left(FailedInsertion)
        else Right(())
      )
    )

我以后可以这样称呼:

  val repo = new DoobieEmployeeRepository(xa)
  val employee = Employee("john", "doe", Set())
  type E[A] = Kleisli[IO, SaveOperation[IO], Either[AppError, A]]
  println(EmployeeService.save[E](employee).run(repo.save).unsafeRunSync())
  println(EmployeeService.save2(employee).run(repo.save).unsafeRunSync())

问题是save 的调用出现以下错误:

Could not find an instance of Monad for E.
I found:

    cats.data.Kleisli.catsDataMonadErrorForKleisli[F, A, E]

But method catsDataMonadErrorForKleisli in class KleisliInstances0_5 does not match type cats.Monad[E].

这个错误对我来说似乎没有意义,因为两个函数的有效签名完全相同,所以单子应该在那里。我怀疑问题出在Ask[F, SaveOperation[F]] 参数上,因为这里的F 不是IO,而SaveOperation 需要IO

为什么我不能使用 Kleisli monad 进行 save 调用?

更新

如果我将类型修改为E[A] = EitherT[[X] =&gt;&gt; Kleisli[IO, SaveOperation[IO], X], AppError, A],我会收到一个新错误:

Could not find an implicit instance of Ask[E, SaveOperation[E]] 

我猜 SaveOperation 的正确泛型类型应该是 IO,但我不知道如何通过 Ask 的实例正确提供它

【问题讨论】:

【参考方案1】:

如果我利用这个机会做一个关于如何改进你的问题的快速教程,我希望你不介意。它不仅增加了有人回答的机会,还可以帮助您自己找到解决方案。

您提交的代码存在一些问题,我的意思是关于 SO 的问题。也许有人可能只看一眼就已经有了现成的答案,但假设他们没有,他们想在工作表中尝试一下。事实证明,您的代码有很多不必要的东西并且无法编译。

以下是您可以采取的一些步骤来改善它:

去除不必要的自定义依赖项,如 EmployeeDoobieEmployeeRepository、错误类型等,并将它们替换为普通 Scala 类型,如 StringThrowable。 只要您仍然可以重现问题,就删除任何剩余的代码。比如savesave2的实现是不需要的,AskRaise也不需要。 确保代码可以编译。这包括添加必要的导入。

通过遵循这些准则,我们得到了这样的结果:

import cats._
import cats.data.Kleisli
import cats.effect.IO

type SaveOperation[F[_]] = String => F[Int]

def save[F[_] : Monad](s: String)(): F[Unit] = ???
def save2(s: String): Kleisli[IO, SaveOperation[IO], Either[Throwable, Unit]] = ???

type E[A] = Kleisli[IO, SaveOperation[IO], Either[Throwable, A]]

println(save[E]("Foo")) // problem!
println(save2("Bar"))

这已经好多了,因为 a) 它允许人们快速试用您的代码,并且 b) 更少的代码意味着更少的认知负担和更少的问题空间。

现在,要检查这里发生了什么,让我们看一些文档: https://typelevel.org/cats/datatypes/kleisli.html#type-class-instances

只要选择的 F[_] 有,它就有一个 Monad 实例。

这很有趣,所以让我们尝试进一步减少我们的代码:

type E[A] = Kleisli[IO, String, Either[Throwable, A]] 
implicitly[Monad[E]] // Monad[E] doesn't exist

好的,但是呢:

type E[A] = Kleisli[IO, String, A] 
implicitly[Monad[E]] // Monad[E] exists!

这是关键发现。而第一种情况下Monad[E]不存在的原因是:

Monad[F[_]] 需要一个类型构造函数; F[_]A =&gt; F[A] 的缩写(请注意,这实际上是 Kleisli[F, A, A] :))。但是,如果我们尝试将 Kleisli 中的值类型“修复”为 Either[Throwable, A]Option[A] 或类似的东西,那么 Monad 实例将不再存在。约定是我们将为Monad 类型类提供一些类型A =&gt; F[A],但现在我们实际上提供了A =&gt; F[Either[Throwable, A]]。 Monad 不那么容易组合,这就是我们有 monad 转换器的原因。

编辑:

经过一番澄清,我想我知道你现在要做什么了。请检查此代码:

  case class Employee(s: String, s2: String)
  case class AppError(msg: String)

  type SaveOperation[F[_]] = Employee => F[Int]

  def save[F[_] : Monad](employee: Employee)(implicit
                                             A: Ask[F, SaveOperation[F]],
                                             R: Raise[F, AppError]): F[Unit] = for 
      s <- A.ask
      rows <- s(employee)
      res <- if (rows != 1) R.raise(AppError("boom"))
      else ().pure[F]
     yield res

  implicit val askSaveOp = new Ask[IO, SaveOperation[IO]] 

    override def applicative: Applicative[IO] =
      implicitly[Applicative[IO]]

    override def ask[E2 >: SaveOperation[IO]]: IO[E2] = 
      val fun = (e: Employee) => IO(println(s"Saved $e!"); 1)
      IO(fun)
    
  

  implicit val raiseAppErr = new Raise[IO, AppError] 

    override def functor: Functor[IO] = 
      implicitly[Functor[IO]]

    override def raise[E2 <: AppError, A](e: E2): IO[A] = 
      IO.raiseError(new Throwable(e.msg))
  

  save[IO](Employee("john", "doe")).unsafeRunSync() // Saved Employee(john,doe)!

我不确定您为什么期望 AskRaise 已经存在,它们指的是自定义类型 EmployeeAppError。也许我错过了一些东西。所以我在这里所做的是我自己实现了它们,并且我也摆脱了你的复杂类型E[A],因为你真正想要的F[_]只是IO。如果您还想拥有Either,那么拥有Raise 并没有多大意义。我认为只使用基于F monad 的代码是有意义的,其中AskRaise 实例可以存储员工并引发错误(在我的示例中,如果您返回@987654357 以外的其他内容,则会引发错误@在执行Ask)。

您能检查一下这是否是您想要实现的目标吗?我们越来越近了。也许您想为任何类型的SaveOperation 输入定义一个通用的Ask,而不仅仅是Employee?对于它的价值,我已经使用过这样的代码库,它们可以很快变成难以阅读和维护的代码。 MTL 很好,但我不想比这更通用。我什至可能更喜欢将保存函数作为参数传递,而不是通过 Ask 实例传递,但这是个人喜好。

【讨论】:

感谢您的回复!它有助于理解问题的第一部分,但是,隐式实例和方法实现在这里很重要,因为目标是编写正确的 MTL 版本。如果我将类型修改为type E[A] = EitherT[[X] =&gt;&gt; Kleisli[IO, SaveOperation[IO], X], AppError, A],我会收到一个新错误,我相信这是我最初试图引用的错误:Could not find an implicit instance of Ask[E, SaveOperation[E]]. SaveOperation 的正确泛型类型应该是IO 我猜,但是正如你所说,这将很难作曲 经过一番摆弄之后,我想我们已经接近在同一页面上。请检查我的编辑。 非常感谢您的回复,当我调用save[IO] 时,它达到了我需要的结果。我最初的计划是打电话给save[Reader[SaveOperation[IO], Either[AppError, *]],我努力为这种结构提供所有的隐含信息。您对IO 的使用与Reader[SaveOperation[IO], Either[AppError, *]] 的使用相比有何想法?后者似乎更具描述性,因为它表明存在执行副作用的依赖项并且结果可能会失败,而普通的IO 除了存在副作用之外并没有说明太多 另外,我使用的 =&gt;&gt; 语法是用于 lambda 类型的 Scala 3 语法,这也是通过 * 和 Scala 2 中的 kind 投影仪插件实现的 有趣,我还没用过 Scala 3。我会假设其余的令人困惑的语法也是由于这个原因:) 我认为你的意图是有道理的,但在这种情况下,我会选择IO[Either[AppError, *]]。不幸的是,猫效应决定让IO 没有错误通道,这与 zio 不同。但让我困惑的是读者——你为什么需要它? Ask 的存在还不足以说明存在依赖关系吗?

以上是关于用于 IO monad 的复杂 monad 转换器的主要内容,如果未能解决你的问题,请参考以下文章

让 F[_] 在接受 IO 的同时实现 Monad trait

在haskell中构建一个非确定性的monad转换器

使用 monad 转换器改变表达式结果

在IO monad中进行递归

Haskell学习-monad

为啥 Haskell 异常只能在 IO monad 中捕获?