复杂的Monad变换器针对IO Monad。

3

我正在尝试编写一个函数的版本,用于将实体保存到数据库。我希望这个函数从环境中读取一些SaveOperation[F[_]]并执行它,并处理可能的失败情况。到目前为止,我想出了两个版本的这个函数: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
为什么我不能对save调用使用Kleisli单子?
更新:如果我将类型修改为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 的实例来正确提供它。
1个回答

4

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

您提交的代码存在几个问题,我是指作为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实例,它就具有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 => F[A]的简写(注意,这实际上是Kleisli[F, A, A] :))。但是如果我们尝试将Kleisli中的值类型“修正”为Either[Throwable, A]Option[A]或类似的任何东西,那么Monad实例就不存在了。契约是我们将提供Monad类型类与某些类型A => F[A],但现在我们实际上正在提供A => F[Either[Throwable, A]]。单子不容易组合,这就是为什么我们有单子变换器的原因。
编辑:
经过澄清,我想现在我知道你想要什么了,请检查此代码:
  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 编写代码并具有存储员工和引发错误的 Ask 和 Raise 实例,是有意义的(例如,在 Ask 的实现中返回除 1 以外的内容将引发错误)。
您能检查这是否是您想要实现的内容吗?我们已经接近成功了。也许您希望为任何种类的 SaveOperation 输入定义一个通用的 Ask,而不仅仅是 Employee?就我个人而言,我曾经使用过这样的代码库,它们很容易变成难以阅读和维护的代码。MTL 是可以的,但我不想比这更通用。我甚至可能更喜欢将保存函数作为参数传递,而不是通过 Ask 实例传递,但这是个人偏好。

谢谢您的回复!这有助于理解问题的第一部分,但是隐式实例和方法实现在这里很重要,因为目标是编写正确的MTL版本。如果我将类型修改为type E[A] = EitherT[[X] =>> Kleisli[IO, SaveOperation[IO], X], AppError, A],我会得到一个新的错误,我相信这就是我最初试图引用的错误:“找不到Ask [E,SaveOperation [E]]的隐式实例。”我猜SaveOperation的正确泛型类型应该是IO,但是正如您所说,这样做将很难组合。 - Leonid Bor
1
经过一番摆弄,我认为我们已经接近于达成共识了。请检查我的编辑。 - slouc
非常感谢您的回复,当我调用save[IO]时,它实现了我需要的结果。我的原始计划是调用save[Reader[SaveOperation[IO], Either[AppError, *]]],但我很难提供这种构造所需的所有隐式参数。您对使用IO与使用Reader[SaveOperation[IO], Either[AppError, *]]有什么想法吗?后者似乎更具描述性,因为它指示存在执行副作用并且结果可能失败的依赖项,而纯IO除了存在副作用外并没有提供太多信息。 - Leonid Bor
有趣,我还没有使用过Scala 3。我会假设其余令人困惑的语法也是由于它 :) 我认为你的意图是有道理的,但在这种情况下,我会选择IO[Either[AppError, *]]。不幸的是,与Zio不同,Cats Effect决定让IO不带错误通道。但令我困惑的是阅读器——为什么你需要它?是否有Ask已经足够说明有一个依赖项了呢? - slouc
当然!请记住,IO 没有显式的错误通道,但这并不意味着没有。每当出现问题时,您可以简单地 IO.raiseError(MyErrorWhichExtendsThrowable(...)),然后在错误处理部分中,您只需模式匹配错误类型以区分例如 AppErrorConnectionError。但是,如果您希望始终在类型签名中编码您的错误和依赖项,则 zio 绝对是您应该尝试的东西。 - slouc
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接