I recently stumbled on the concept of a Kleisli and every tutorial/link/reference that I read motivates the use of Kleisli via the following constructs:
f: a -> m[b]
with g: b -> m[c]
- I think the very definition of a monad already captures this case - do/bind/for/flatMap
do that. One needn't lean on the Kleisli construct to achieve this. So this cannot be the "primary" use case of a Kleisli IMO.Config
injected then a Kleisli construct can be used to abstract away the repeatable injection. There are numerous ways of achieving this (for example with implicit
s in Scala) that invoking a Kleisli may not be necessary. Again, IMO this doesn't stand out as a "primary" use case.M1[M2[M1[M2[a]]]]
could be transformed to [M1[M2[a]]]
which could (I may be wrong) be flattened across monadic boundaries to be composable with an a -> M3[b]
(say). For this one could us a Kleisli triple and invoke the construct since if you were to do it from scratch you may just reinvent the Kleisli. This seems to be a good candidate for justifying the use of a Kleisli. Is this correct?I believe #1-#2
above are "secondary uses". That is, if you do happen to use the Kleisli construct, you can also get patterns for composing functions that return monads as well as config injection. However, they cannot be motivating problems advocating the power of Kleislis.
Under the assumption of using the least powerful abstraction to solve the problem at hand, what motivating problems can be used to showcase their use?
Alternate Thesis: It's entirely possible that I am totally wrong and my understanding of Kleislis is incorrect. I lack the necessary category theory background, but it could be that a Kleisli is an orthogonal construct that can be used in place of monads and they (Kleisli) are a category theoretic lens through which we view the problems of the functional world (i.e., a Klesli simply wraps a monadic function a -> M[b]
and now we can work at a higher level of abstraction where the function is the object of manipulation vs an object of usage). Thus, the use of Kleisli can be simply understood to be "Functional Programming with Kleisli". If this is true, then there ought to be a situation where a Kleisli can solve a problem better than existing constructs and we circle back to the issue of a motivating problem. It's equally likely, that there isn't such a motivating problem per se, if it's simply a lens which offers different solutions to the same problem. Which is it?
It'd be really helpful to get some input be able to reconstruct the need for Kleislis.
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".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With