Reputation: 249
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.
Upvotes: 2
Views: 288
Reputation: 249
I've got the best answer for this from Daniel Ciocîrlan (author of RockTheJVM courses, which are great, btw).
Daniel Ciocîrlan: Kleisli is contravariant in the input type (second type argument), so NoDependencies <: DbSessionDependency. But since you pass a PlainDecoder in the make factory method, you expect PlainDecoder = Decoder[NoDependencies] <: Decoder[DbSessionDependency]. That can only happen if Decoder is covariant in F. So you need to have Decoder[+F[_]].
So, it works when
trait Decoder[+F[_]] {
def decode(s: String): F[String]
}
Upvotes: 1
Reputation: 27595
Kleisli
(as in Cats' ReaderT
monad implementation) is contravariant at input type:
final case class Kleisli[F[_], -A, B](run: A => F[B]) { self =>
...
which means that Kleisli[ErrorOr, DbSession, A]
is not a subtype of Kleisli[ErrorOr, Any, A]
and cannot be upcased to it.
It's the other way round, Kleisli[ErrorOr, Any, A]
is a subtype of Kleisli[ErrorOr, DbSession, A]
.
If you think that Kleisli[F, In, Out]
here models In => F[Out]
then you can notice that DbSession => F[Out]
is accepting less inputs than Any => F[Out]
. You could use Any => F[Out]
as DbSession => F[Out]
but not the other way round because all DbSession
inputs are also valid Any
inputs, but not all Any
inputs are valid DbSession
inputs (someone can pass e.g. Unit
or Int
). 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 be Kleisli[ErrorOr, Any, A]
. If you combine two Kleislis, one taking Any
and the other taking DbSession
the input inferred should be Kleisli[ErrorOr, DbSession, A]
.
Upvotes: 3