具有 Tagless Final 样式的 Kleisli 依赖项

问题描述

我正在尝试使用 Kleisli 为依赖项建模。例如,假设我有以下业务逻辑类型:

import $ivy.`org.typelevel:cats-core_2.13:2.2.0`
import cats._
import cats.implicits._

trait Decoder[F[_]] {
    def decode(s: String): F[String]
}

trait DatabaseAccess[F[_]] {
    def getData(): F[String]
}

trait BusinessLogicService[F[_]] {
    def getTheProcessedData(): F[String]
}

object BusinessLogicService {
    def make[F[_]: Monad](
        decoder: Decoder[F],db: DatabaseAccess[F]
    ): BusinessLogicService[F] =
        new BusinessLogicService[F] {
            override def getTheProcessedData(): F[String] = for {
                str <- db.getData()
                decodedStr <- decoder.decode(str)
            } yield decodedStr
        }
}

现在我有以下解码和数据库访问的实现:

import cats.data.Kleisli
trait DbSession {
    def runQuery(): String
}

type ErrorOr[A] = Either[Throwable,A]

type DbSessionDependency[A] = Kleisli[ErrorOr,DbSession,A]
type NoDependencies[A] = Kleisli[ErrorOr,Any,A]

object PlainDecoder extends Decoder[NoDependencies] {
    override def decode(s: String): NoDependencies[String] =
        Kleisli { _ => Right(s.toLowerCase()) }
}

object Sessioneddbaccess extends DatabaseAccess[DbSessionDependency] {
    override def getData(): DbSessionDependency[String] = Kleisli { s =>
        Right(s.runQuery)
    }
}

现在,当我想将两个对象与业务逻辑一起使用时,我遇到了类型冲突: Kleisli[ErrorOr,A] 与 Klesili[ErrorOr,A] 不兼容。

val businessLogic: BusinessLogicService[DbSessionDependency] = 
    BusinessLogicService.make(PlainDecoder,Sessioneddbaccess)

像这样组成类的最“正确”的方式是什么?我不想让我的解码器需要数据库会话,而且我也不想在解码器周围创建副本/包装器。

解决方法

Kleisli(如 Cats 的 ReaderT monad 实现)在输入类型上是逆变的:

final case class Kleisli[F[_],-A,B](run: A => F[B]) { self =>
...

这意味着 Kleisli[ErrorOr,DbSession,A] 不是 Kleisli[ErrorOr,Any,A] 的子类型,不能向上转换。

相反,Kleisli[ErrorOr,A]Kleisli[ErrorOr,A] 的子类型。

如果您认为 Kleisli[F,In,Out] 在这里模拟 In => F[Out],那么您会注意到 DbSession => F[Out] 接受的输入少于 Any => F[Out]。您可以将 Any => F[Out] 用作 DbSession => F[Out] 但不能反过来,因为所有 DbSession 输入也是有效的 Any 输入,但并非所有 Any 输入都是有效的 { {1}} 输入(有人可以传递例如 DbSessionUnit)。所以唯一安全的方法是让输入更具体(为不太具体的输入定义的函数总是可以处理更具体的输入)。

这是通过 Int 参数中的逆变建模的,这意味着超类型总是更具体。因此,在您的情况下,您不能期望推断类型为 In。如果将两个 Kleislis 组合起来,一个取 Kleisli[ErrorOr,A],另一个取 Any,则推断的输入应为 DbSession

,

我从 Daniel Ciocîrlan(RockTheJVM 课程的作者,很棒,顺便说一句)那里得到了最好的答案。

Daniel Ciocîrlan:Kleisli 在输入类型(第二类型参数)中是逆变的,所以 NoDependencies <: dbsessiondependency make factory plaindecoder decoder f>

所以,它在什么时候起作用

trait Decoder[+F[_]] {
    def decode(s: String): F[String]
}