2

我正在尝试使用 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, DbSession, A] 与 Klesili[ErrorOr, Any, A] 不兼容。

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

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

4

2 回答 2

3

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

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

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

反过来,它Kleisli[ErrorOr, Any, A]是 的子类型Kleisli[ErrorOr, DbSession, A]

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

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

于 2021-01-04T10:28:08.533 回答
1

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

Daniel Ciocîrlan:Kleisli 在输入类型(第二类型参数)中是逆变的,所以 NoDependencies <: DbSessionDependency。但是由于您在 make 工厂方法中传递了 PlainDecoder,因此您期望 PlainDecoder = Decoder[NoDependencies] <: Decoder[DbSessionDependency]。这只有在解码器在 F 中是协变的情况下才会发生。所以你需要解码器[+F[_]]。

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

trait Decoder[+F[_]] {
    def decode(s: String): F[String]
}
于 2021-01-04T10:54:42.750 回答