Kleisli dependencies with Tagless Final style

252 Views Asked by At

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.

2

There are 2 best solutions below

1
On

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].

0
On

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]
}