When should one use a Kleisli?

Kleisli aka ReaderT is from practical point of view #2 (and as I show later #3) – dependency injection of one the same component into several functions. If I have:

val makeDB: Config => IO[Database]
val makeHttp: Config => IO[HttpClient]
val makeCache: Config => IO[RedisClient]

then I could combine things as a monad this way:

def program(config: Config) = for {
  db <- makeDB(config)
  http <- makeHttp(config)
  cache <- makeCache(config)
  ...
} yield someResult

but passing things manually would be annoying. So instead we could make that Config => part of the type and do our monadic composition without it.

val program: Kleisli[IO, Config, Result] = for {
  db <- Kleisli(makeDB)
  http <- Kleisli(makeHttp)
  cache <- Kliesli(makeCache)
  ...
} yield someResult

If all of my functions were Kleisli in the first place, then I would be able to skip that Kleisli(...) part of the for comprehension.

val program: Kleisli[IO, Config, Result] = for {
  db <- makeDB
  http <- makeHttp
  cache <- makeCache
  ...
} yield someResult

And here comes another reason why this might be popular: tagless final and MTL. You could define that your function somehow uses Config to run and make it its contract, but without specifying how and what kind of F[_] you exactly have:

import cats.Monad
import cats.mtl.ApplicativeAsk

// implementations will summon implicit ApplicativeAsk[F, Config]
// and Monad[F] to extract Config and use it to build a result
// in a for comprehension
def makeDB[F[_]: Monad: ApplicativeAsk[*, Config]]: F[Database]
def makeHttp[F[_]: Monad: ApplicativeAsk[*, Config]]: F[HttpClient]
def makeCache[F[_]: Monad: ApplicativeAsk[*, Config]]: F[RedisClient]

def program[F[_]: Monad: ApplicativeAsk[*, Config]]: F[Result] = for {
  db <- makeDB
  http <- makeHttp
  cache <- makeCache
  ...
} yield result

If you define type F[A] = Kleisli[IO, Cache, A] and provide necessary implicits (here: Monad[Kleisli[IO, Cache, *]] and ApplicativeAsk[Kleisli[IO, Cache, *], Cache]) you will be able to run this program the same way as the previous example with Kleisli.

BUT, you could switch cats.effect.IO to monix.eval.Task. You combine several monad transformers e.g. ReaderT and StateT and EitherT. Or 2 different Kleisli/ReaderT to inject 2 different dependencies. And because Kleisli/ReaderT is “just simple type” that you can compose with other monads, you can stack things together to your needs. With tagless final and MTL, you can separate the declarative requirement of your program where you write down what each component needs to work (and then be able to use with extension methods), from the part where you define the actual type which will be used, and which you can build from smaller, simpler building blocks.

As far as I can tell this simplicity and composability is why many people use Kleisli.

That said, there are alternative approaches to designing solutions in such cases (e.g. ZIO defines itself in such a way that monad transformers should not be required) while many people simply write their code the way that wouldn’t make them require anything monad transformer-like.

As for your concern about category theory Kleisli is

one of two extremal solutions to the question “Does every monad arise from an adjunction?”

however I wouldn’t be able to point at many programmers who use it daily and bother with this motivation at all. At least I don’t know personally anyone who treats this as anything else than “occasionally useful utility”.

Leave a Comment