IO monad 的复杂 monad 转换器

Complex monad transformer for IO monad

我正在尝试编写 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].

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

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

更新:

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

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

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

的实例正确提供它

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

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

以下是您可以采取的一些改进措施:

  • 删除不必要的自定义依赖项,如 EmployeeDoobieEmployeeRepository、错误类型等,并用 StringThrowable 等普通 Scala 类型替换它们。
  • 删除任何剩余的代码,只要您仍然可以重现问题。例如,不需要 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) 更少的代码意味着更少的认知负担和更少的问题space。

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

It has a Monad instance as long as the chosen F[_] does.

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

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 => F[A] 的缩写(请注意,这实际上是 Kleisli[F, A, A] :))。但是,如果我们尝试将 Kleisli 中的值类型“固定”为 Either[Throwable, A]Option[A] 或类似的任何类型,那么 Monad 实例将不再存在。合同是我们将为 Monad 类型类提供某种类型 A => F[A],但现在我们实际上提供 A => 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 实例可以存储员工并引发错误(在我的示例中,如果你return 在 Ask 的实现中 1 以外的东西。

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