I am trying to model a dependency using Kleisli. For instance, let's imagine I have the following business logic types:
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
}
}
And now I have the following implementations for Decode and DatabaseAccess:
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)
}
}
Now when I want to use both objects with the business logic I have a conflict of type: Kleisli[ErrorOr, DbSession, A] is not compatible with Klesili[ErrorOr, Any, A].
val businessLogic: BusinessLogicService[DbSessionDependency] =
BusinessLogicService.make(PlainDecoder, SessionedDbAccess)
What would be the most "correct" way of composing the classes like this? I don't want to make my decoder to require the database session and I'm not really into creating a copy/wrapper around the Decoder as well.
Kleisli
(as in Cats'ReaderT
monad implementation) is contravariant at input type:which means that
Kleisli[ErrorOr, DbSession, A]
is not a subtype ofKleisli[ErrorOr, Any, A]
and cannot be upcased to it.It's the other way round,
Kleisli[ErrorOr, Any, A]
is a subtype ofKleisli[ErrorOr, DbSession, A]
.If you think that
Kleisli[F, In, Out]
here modelsIn => F[Out]
then you can notice thatDbSession => F[Out]
is accepting less inputs thanAny => F[Out]
. You could useAny => F[Out]
asDbSession => F[Out]
but not the other way round because allDbSession
inputs are also validAny
inputs, but not allAny
inputs are validDbSession
inputs (someone can pass e.g.Unit
orInt
). So the only safe way is to make input more specific (function defined for less specific input can always handle more specific input).This is modeled by contravariance in
In
parameter, meaning that supertypes are always more specific. So in your case you cannot expect the inferred type to beKleisli[ErrorOr, Any, A]
. If you combine two Kleislis, one takingAny
and the other takingDbSession
the input inferred should beKleisli[ErrorOr, DbSession, A]
.