用于 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] =>> Kleisli[IO, SaveOperation[IO], X], AppError, A]
,我会收到一个新错误:
Could not find an implicit instance of Ask[E, SaveOperation[E]]
我猜 SaveOperation 的正确泛型类型应该是 IO,但我不知道如何通过 Ask
的实例正确提供它
【问题讨论】:
【参考方案1】:如果我利用这个机会做一个关于如何改进你的问题的快速教程,我希望你不介意。它不仅增加了有人回答的机会,还可以帮助您自己找到解决方案。
您提交的代码存在一些问题,我的意思是关于 SO 的问题。也许有人可能只看一眼就已经有了现成的答案,但假设他们没有,他们想在工作表中尝试一下。事实证明,您的代码有很多不必要的东西并且无法编译。
以下是您可以采取的一些步骤来改善它:
去除不必要的自定义依赖项,如Employee
、DoobieEmployeeRepository
、错误类型等,并将它们替换为普通 Scala 类型,如 String
或 Throwable
。
只要您仍然可以重现问题,就删除任何剩余的代码。比如save
和save2
的实现是不需要的,Ask
和Raise
也不需要。
确保代码可以编译。这包括添加必要的导入。
通过遵循这些准则,我们得到了这样的结果:
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 => 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)!
我不确定您为什么期望 Ask
和 Raise
已经存在,它们指的是自定义类型 Employee
和 AppError
。也许我错过了一些东西。所以我在这里所做的是我自己实现了它们,并且我也摆脱了你的复杂类型E[A]
,因为你真正想要的F[_]
只是IO
。如果您还想拥有Either
,那么拥有Raise
并没有多大意义。我认为只使用基于F
monad 的代码是有意义的,其中Ask
和Raise
实例可以存储员工并引发错误(在我的示例中,如果您返回@987654357 以外的其他内容,则会引发错误@在执行Ask
)。
您能检查一下这是否是您想要实现的目标吗?我们越来越近了。也许您想为任何类型的SaveOperation
输入定义一个通用的Ask
,而不仅仅是Employee
?对于它的价值,我已经使用过这样的代码库,它们可以很快变成难以阅读和维护的代码。 MTL 很好,但我不想比这更通用。我什至可能更喜欢将保存函数作为参数传递,而不是通过 Ask
实例传递,但这是个人喜好。
【讨论】:
感谢您的回复!它有助于理解问题的第一部分,但是,隐式实例和方法实现在这里很重要,因为目标是编写正确的 MTL 版本。如果我将类型修改为type E[A] = EitherT[[X] =>> 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
除了存在副作用之外并没有说明太多
另外,我使用的 =>>
语法是用于 lambda 类型的 Scala 3 语法,这也是通过 *
和 Scala 2 中的 kind 投影仪插件实现的
有趣,我还没用过 Scala 3。我会假设其余的令人困惑的语法也是由于这个原因:) 我认为你的意图是有道理的,但在这种情况下,我会选择IO[Either[AppError, *]]
。不幸的是,猫效应决定让IO
没有错误通道,这与 zio 不同。但让我困惑的是读者——你为什么需要它? Ask
的存在还不足以说明存在依赖关系吗?以上是关于用于 IO monad 的复杂 monad 转换器的主要内容,如果未能解决你的问题,请参考以下文章