40

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:

  1. Composing functions that return monads: 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.
  2. Inserting configuration: This one states that if multiple objects (types, case/data classes etc.,) need to have a 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 implicits in Scala) that invoking a Kleisli may not be necessary. Again, IMO this doesn't stand out as a "primary" use case.
  3. Monad Transformers: I don't have a solid understanding of this but here's my interpretation: If you have the need of "composing monads" you need a construct that allows you to parameterize the monads themselves. For example 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.

duplode
  • 33,731
  • 7
  • 79
  • 150
PhD
  • 11,202
  • 14
  • 64
  • 112
  • 3
    Kleisli is not some big important thing that needs to be strongly motivated to use. It's just a way you can tilt your head when there's a monad around. You can think of "monadic values" as the main thing, or you can think of "effectful transformations" as the main thing, and the concept of kliesli arrows is the relationship between them. Like many things in this abstract world, the power comes from compositions. E.g. "The thing I need is the `Choice` transform of the `Kleisli` arrow of `List`", and just like that you have the appropriate powerful foundation to express your ideas. – luqui May 20 '20 at 00:08
  • @luqui - you are probably right. I understand it's not a "big important thing" - my confusion stems from _what is this thing_ and _when/where/how_ can I grasp its usefulness. Perhaps Kleisli offers a "lens" as you suggested where you can think of "monadic values" or "effectful transformations" as the _main thing_. Once you select the POV you can express your ideas accordingly. More like a _frame of reference_ IMO. From my current POV a Kleisli doesn't seem to warrant attention. However, I am _curious_ that if I gave it attention, what can I learn about it from a _meta_ POV. – PhD May 20 '20 at 01:46
  • 2
    "Kleisli" by itself isn't a thing. The concept of a Kleisli *arrow* comes from category theory, but in Haskell you can think of it as a generalization of a function. Given a function `f :: a -> b` and a monad `m`, the corresponding Kleisli arrow is a new function `f' :: a -> m b`. (The same applies to Scala, I assume, using different syntax.) – chepner May 20 '20 at 12:24

3 Answers3

20

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

PhD
  • 11,202
  • 14
  • 64
  • 112
Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
  • This is illuminating. I definitely appreciate the `ReaderT` POV and I have an understanding of the motivation of needing a `Reader` Monad. As you said, if *all functions use Kleisli...* seems to edify my notion of it being an alternate view of computations where the functions are wrapped into Kleisli and then using *that* lens to solve the problem. But again, as you said, not many bother with this it's probably an "additional utility" in the "Category theory for Programmers" toolbox – PhD May 19 '20 at 23:29
10

Preliminary note: this is very much a Haskell-centric answer.

On #1, luqui's comment puts it very nicely:

Kleisli is not some big important thing that needs to be strongly motivated to use. It's just a way you can tilt your head when there's a monad around.

If you have some chained binds...

m >>= f >>= g >>= h

... the associativity monad law allows you to rewrite them as...

m >>= \a -> f a >>= \b -> g b >>= \c -> h c

... or, equivalently...

m >>= (f >=> g >=> h)

... where (>=>) is an operator that performs Kleisli composition:

(>=>)       :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
f >=> g     = \x -> f x >>= g

Besides giving us a nicer presentation of the monad laws than the one using bind, (>=>) is occasionally an ergonomic way to write monadic computations. One illustration I can think of is the xml-conduit library; for instance, the following snippet was taken from a chapter of the Yesod book:

main :: IO ()
main = do
    doc <- readFile def "test2.xml"
    let cursor = fromDocument doc
    print $ T.concat $
        cursor $// element "h2"
               >=> attributeIs "class" "bar"
               >=> precedingSibling
               >=> element "h1"
               &// content 

Here, the XML axes are implemented as list monad Kleisli arrows. In this context, using (>=>) to compose them without explicitly mentioning what they are being applied to feels very natural.


On #2, between your question and Mateusz Kubuszok's answer, I have just learned that some of the relevant Scala-centric documentation identifies ReaderT and Kleisli on the basis of both having Monad m => a -> m b as their underlying type. Still, I'd say a clearer picture is obtained by thinking of ReaderT and Kleisli as expressing different concepts, even if their implementations happen to coincide in some sense. In particular, the kind of composition done through (>=>) or the Category instance of Kleisli feels alien with respect to how ReaderT is typically used, namely, for expressing dependency on a fixed environment.


On #3, I believe that is only tangentially related to Kleisli. The question of when the composition of monads results in a monad and related matters about monad transformers are not solved through recourse to Kleisli arrows. While thinking in terms of Kleisli arrows and Kleisli categories when dealing with such matters is sometimes useful, I'd say that is primarily because Kleisli arrows and categories are generally an useful way to consider monads, and not because of some more specific connection.

duplode
  • 33,731
  • 7
  • 79
  • 150
  • This is great! You are probably right in pointing out that `ReaderT` and `Kleisli` _happen to coincide in their implementation_ and perhaps that added to my confusion and hence the call outs for `#2 and #3` in my question. I'm beginning to think that a Kleisli is more of a POV (like you and @luqui) mentioned that you can use _when you have a monad lying around_. Question: _Why_ did you go from `m >>= f >>= g >>= h` to `m >>= (f >==> g >==>h)`? I'm not sure I understand the need... – PhD May 20 '20 at 17:44
  • @PhD On your question: there was no pressing need; that was just an unspecific illustration of the change of POV. Ultimately they are two ways of writing the same thing, though one or the other might feel nicer to use in specific situations. – duplode May 20 '20 at 19:16
0

Sometimes we might want to structure computations in a way that is less expressive, more "rigid" than the full Monad interface, but also potentially more inspectable. Kleislis can be used to embed monadic effects into those computations.

For example, imagine that we are building computation pipelines where each step has some kind of annotation attached. An annotation could represent an estimation of the time to complete the step, or of some other resource related to the step. We want to be able to inspect the accumulated annotations for a whole pipeline before actually "running" its effects:

import Prelude hiding (id,(.))
import Control.Category (Category,(.),id)
import Control.Arrow (Kleisli (..))

data Step w m i o = Step w (Kleisli m i o) 

instance (Monoid w, Monad m) => Category (Step w m) where
    id = Step mempty (Kleisli pure)
    (.) (Step wf f) (Step wg g) = Step (wg <> wf) (f . g)

Putting it to work:

main :: IO ()
main = do
    let Step w (Kleisli _) = 
              Step "b" (Kleisli putStrLn) 
            . Step "a" (Kleisli (\() -> getLine))
    putStrLn w
    -- result: ab
danidiaz
  • 26,936
  • 4
  • 45
  • 95