问题描述
我正在尝试使用 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}} 输入(有人可以传递例如 DbSession
或 Unit
)。所以唯一安全的方法是让输入更具体(为不太具体的输入定义的函数总是可以处理更具体的输入)。
这是通过 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]
}