Kleisli category

Microservices, composable APIs

Published:
 _  ___     _____ ___ ____  _     ___
| |/ / |   | ____|_ _/ ___|| |   |_ _|
| ' /| |   |  _|  | |\___ \| |    | |
| . \| |___| |___ | | ___) | |___ | |
|_|\_\_____|_____|___|____/|_____|___|__  ___ _____ ____
 / ___|  / \|_   _| ____/ ___|/ _ \|  _ \|_ _| ____/ ___|
| |     / _ \ | | |  _|| |  _| | | | |_) || ||  _| \___ \
| |___ / ___ \| | | |__| |_| | |_| |  _ < | || |___ ___) |
 \____/_/   \_\_| |_____\____|\___/|_| \_\___|_____|____/

We have been talking a lot about monads recently. We looked at their properties, usage, best practices. But how do they fit into category theory? We know about their relationship to monoids.


Semigroup  -->  Monoid
                        \
                         \
                          ---> Monad <---- Natural transformation
                         /\
Functor --> Applicative    \
                           Endofunctor

Guess what. That's not all. All the concepts that we introduced tend to find their place among the rest in a form of a classification. And monads are not the exception. Yes, we are going to be talking about the categories again...

Every monad gives rise to a Kleisli category. And that is the missing piece that forms the full circle.

Kleisli category #

Remember when we learned that a category consists of objects, morphisms between objects, and rules for composition? Well, every monad gives rise to a special category called its Kleisli category, where:

  • Objects are the same as in the original category
  • Morphisms are "monadic functions" of the form a -> M b (where M is our monad)
  • Composition is defined using the monad's bind operation

It means that when you write flatMap or >>= in your code, you're actually performing categorical composition in the Kleisli category!

Think about all those monadic functions you've written:

  • String -> Maybe Int (parsing that might fail)
  • UserId -> IO User (database lookup with side effects)
  • Config -> State AppState String (stateful computations)

Each of these has the shape a -> M b, and it turns out they form morphisms in a category. When you chain them together with monadic bind, you're composing morphisms in the Kleisli category. The monad laws we learned? They're just the category laws in disguise!

This connection explains why the monad laws ensure that composition works as expected. It's because monads are secretly categories, and we've been doing category theory all along without realizing it.

Formal definition #

Given a category C and a monad T = (T, η, μ) (definition) on C, where T: C -> C is an endofunctor, η: Id_C ⟹ T is unit, and μ: T ∘ T ⟹ T is multiplication, the Kleisli category C_T (also denoted Kl(T)) is defined as follows:

  1. Objects: The objects of C_T are the same as the objects of C. If A is an object in C, then A is also an object in C_T.

  2. Morphisms: The morphisms in C_T are different from those in C:

  • A morphism from object A to object B in C_T is a morphism f: A -> TB in the original category C
  • We write this as f: A -> B in C_T to distinguish Kleisli morphisms from regular morphisms

In other words: Hom_{C_T}(A, B) = Hom_C(A, TB)

Identity Morphisms #

For each object A in C_T, the identity morphism id_A: A -> A is defined as:

id_A = η_A: A -> TA

where η_A is the unit of the monad T at object A.

Composition #

Given two Kleisli morphisms:

  • f: A -> B (which is f: A -> TB in C)
  • g: B -> C (which is g: B -> TC in C)

Their composition g ∘_T f: A -> C is defined as:

  • g ∘_T f = μ_C ∘ T(g) ∘ f: A -> TC

where:

  • μ_C: TTC -> TC is the multiplication (join) of the monad at object C
  • T(g): TB -> TTC is the functor T applied to morphism g

Composition in Terms of Bind #

In programming terms, if we have:

  • f: A -> M B
  • g: B -> M C

Then their Kleisli composition is: g ∘_T f = λx. f(x) >>= g

or equivalently: g ∘_T f = λx. flatMap(f(x), g)

Category Laws #

The Kleisli category satisfies the category laws because of the monad laws:

Left Identity: id_B ∘_T f = f

  • Follows from the right unit law: μ ∘ Tη = id

Right Identity: f ∘_T id_A = f

  • Follows from the left unit law: μ ∘ ηT = id

Associativity: (h ∘_T g) ∘_T f = h ∘_T (g ∘_T f)

  • Follows from the associativity law: μ ∘ Tμ = μ ∘ μT

Set category example #

Sets form categories like so:

  • Objects: All sets (collections of elements) Examples: (natural numbers), (real numbers), {1, 2, 3}, ∅ (empty set), etc.

  • Morphisms: Functions between sets A morphism from set A to set B is a function f: A -> B Each element in A maps to exactly one element in B

  • Identity Morphisms For each set A, the identity function id_A: A -> A id_A(x) = x for all x ∈ A

  • Composition Function composition: if f: A -> B and g: B -> C, then g ∘ f: A -> C (g ∘ f)(x) = g(f(x))

  • Category Laws

    • Left Identity: id_B ∘ f = f
    • Right Identity: f ∘ id_A = f
    • Associativity: h ∘ (g ∘ f) = (h ∘ g) ∘ f

In the category Set with the Maybe monad, Kleisli category looks like this:

  • Objects: All sets (same as in Set)
  • Morphisms A -> B: Functions A -> Maybe B
  • Identity: η_A(x) = Some(x)
  • Composition: (g ∘_T f)(x) = f(x) >>= g

Kleisli Arrow #

Now that we understand the formal structure, let's dive deeper into the key concept that makes Kleisli categories work: the Kleisli arrow.

A Kleisli arrow is simply a function of the form A -> M B, where:

  • A is the input type
  • B is the output type
  • M is a monad

These arrows are the building blocks of the Kleisli category. Every morphism in a Kleisli category is a Kleisli arrow.

Let's see how regular functions transform into Kleisli arrows:

Regular function composition:

f: A -> B g: B -> C g ∘ f: A -> C

Kleisli arrow composition:

f: A -> M B (Kleisli arrow) g: B -> M C (Kleisli arrow) g <=< f: A -> M C (Kleisli composition)

The key difference is that Kleisli arrows can "fail", have "side effects", or produce "multiple results" - depending on the monad M.

Kleisli Composition Operator #

In Haskell notation, a Kleisli arrow looks like this:

newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b }

There is a special built-in operator for Kleisli composition: <=< (read as "fish")

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

This is the "flipped" version of bind that makes the composition direction match mathematical notation.

Examples of Kleisli Arrows #

Partial Functions (Maybe monad)

parseInt :: String -> Maybe Int
sqrt :: Int -> Maybe Int

-- Composition: parse then take square root
parseAndSqrt :: String -> Maybe Int
parseAndSqrt = sqrt <=< parseInt

Effectful Functions (IO monad)

readUserId :: () -> IO Int
fetchUser :: Int -> IO User

-- Composition: read ID then fetch user
getUserFromInput :: () -> IO User
getUserFromInput = fetchUser <=< readUserId

Stateful Functions (State monad)

incrementCounter :: () -> State Int Int
doubleValue :: Int -> State Int Int

-- Composition: increment then double
incAndDouble :: () -> State Int Int
incAndDouble = doubleValue <=< incrementCounter

Kleisli Extension #

Kleisli arrows represent a shift in thinking:

  • Regular functions: Pure transformations A -> B
  • Kleisli arrows: Computations that produce B from A in context M

This perspective is powerful because it lets us treat "computations" as first-class objects that can be composed, analyzed, and reasoned about categorically.

But since a Kleisli arrow is just a morphism, we need something on top to extend it to monadic inputs.

The Kleisli extension is the operation that lets you lift an ordinary Kleisli arrow f: A -> T B into a monadic bind on T A.

Formally: given f: A -> T B, the Kleisli extension f*: T A -> T B is defined by f* = μ_B ∘ T f

  • T f: T A -> T(T B) - apply the functor inside the monad
  • μ_B: T(T B) -> T B - the monad’s multiplication or "join" at object B

In Haskell:

-- given
f :: a -> m b
-- we define
fstar :: m a -> m b
fstar ma = ma >>= f

This is exactly bind in Haskell: take a monadic value and a Kleisli arrow, and chain them.

  • A Kleisli arrow is a morphism A -> T B
  • The Kleisli extension takes such an arrow and extends it to work on T A

So Kleisli extension is the machinery that makes sequencing possible: it’s what turns applicative-style combination into full monadic chaining.

Kleisli Triple Definition #

There is one more tiny piece of the puzzle left. One more way to represent monads.

A monad can equivalently be defined as a Kleisli triple (M, η, (-)^*) where:

  • M: Obj(C) -> Obj(C) maps objects
  • η_A: A -> M(A) for each object A (unit)
  • for every Kleisli arrow f: A -> M(B), the extension f*: M(A) -> M(B) lifts f to monadic inputs

The Kleisli extension satisfies:

  • Left identity: f* ∘ η_A = f
  • Right identity: (η_A)* = id_{M(A)}
  • Associativity: g* ∘ f* = (g* ∘ f)*

Kleisli composition is then induced by extension: for f: A -> M(B) and g: B -> M(C), define g • f = g* ∘ f.

The Kleisli triple reveals that monads are fundamentally about composition. Instead of thinking about bind and return as separate operations, we see that monads are really about defining a new kind of composition () that handles effects automatically.

When language designers create new monadic syntax, they're essentially providing convenient ways to write Kleisli composition.

The traditional monad definition requires understanding functors, natural transformations, and complex coherence conditions. The Kleisli triple gives us the same expressive power with three simple extension laws that directly correspond to how we actually use monads in practice. The Kleisli triple makes the categorical nature of monads explicit.

Remember lax monoidal discussion we had? Back then, we established that an applicative functor is a lax monoidal functor - it can combine independent effects. And every monad is an applicative, but not every applicative is a monad. Finally, the picture is crystal clear now:

  • Applicative = lax monoidal functor - can combine independent effects.
  • Monad = lax monoidal functor + Kleisli extension - can chain dependent effects.
    Applicative (lax monoidal)
        │
        │  add Kleisli extension (>>=)
        ▼
      Monad

Every Kleisli triple defines a category, and every monad gives rise to a Kleisli category.

In essence, the Kleisli triple shows us that when we write:

parseAge >=> validateAge

We're not just "chaining monadic operations" - we're composing morphisms in a well-defined structure that guarantees our composition will work correctly and efficiently.

Examples #

Let's see how Kleisli arrows and Kleisli composition work in practice across different languages and monads.

Safe Division Chain #

Chain multiple division operations that might fail.

User Validation Pipeline #

Validate user data through multiple steps, short-circuiting on the first error.

Triangle Kleisli example #

Let's consider a Kleisli category built from triangles where morphisms are geometric transformations that might fail or produce multiple results. Unlike regular geometric transformations, Kleisli arrows wrap results in a monad to handle uncertainty, errors, or multiple outcomes.

Original Category: Triangles with deterministic transformations Kleisli Category: Triangles with potentially failing/uncertain transformations

Objects: Different triangles in the plane

  • T1: An equilateral triangle
  • T2: A right triangle
  • T3: An isosceles triangle
  • T4: A scalene triangle

Kleisli Arrows: Transformations that might fail or produce uncertain results

Hom_Kl(T1, T2): Kleisli arrows from equilateral to right triangle

T1 (Equilateral)          Maybe(T2) (Potentially right triangle)
    △                         ⟨ |\  ⟩
   /A\                        ⟨ | \ ⟩
  / | \                       ⟨ |  \⟩
 /B___C\                      ⟨ |___\⟩
                              or Nothing

1. Kleisli arrows in Hom_Kl(T1, T2):

- attemptShear: T1 -> Maybe(T2)    (might fail if angle too steep)
- tryScale: T1 -> Maybe(T2)        (might fail if proportions impossible)
- conditionalRotate: T1 -> Maybe(T2) (succeeds only under certain conditions)

2. Self-Kleisli Arrows: `Hom_Kl(T1, T1)` - Transformations that preserve type but might fail

T1 (Equilateral triangle)          Maybe(T1)
    △                               ⟨  △  ⟩
   /A\                              ⟨ /A\ ⟩
  / | \                             ⟨/ | \⟩
 /B___C\                            ⟨B___C⟩
                                    or Nothing

3. Kleisli arrows in Hom_Kl(T1, T1):

- safeRotate: T1 -> Maybe(T1)        (rotation that might fail if constraints violated)
- validateReflect: T1 -> Maybe(T1)   (reflection that checks triangle validity)
- return/pure: T1 -> Maybe(T1)       (always succeeds: pure(triangle))
- conditionalScale: T1 -> Maybe(T1)  (scaling that might fail if size limits exceeded)

4. Kleisli Composition: Chaining potentially failing transformations

Composing Kleisli arrows:

T1 --attemptShear--> Maybe(T2) --validateRotate--> Maybe(T3)

Where:
- attemptShear ∈ Hom_Kl(T1, T2): T1 → Maybe(T2)
- validateRotate ∈ Hom_Kl(T2, T3): T2 → Maybe(T3)

5. Kleisli composition (attemptShear >=> validateRotate):

    1. Apply attemptShear to T1
        - If succeeds: get Some(T2), proceed to step 2
        - If fails: get Nothing, entire composition fails
    2. If we have Some(T2), apply validateRotate
        - If succeeds: get Some(T3)
        - If fails: get Nothing

    △     attemptShear    ⟨ |\ ⟩   validateRotate   ⟨  △  ⟩
   / \    ----------->    ⟨ | \⟩   ------------->   ⟨ / \ ⟩
  /   \                   ⟨ |  \⟩                   ⟨/   \⟩
 /_____\                  ⟨ |___\⟩                  ⟨_____⟩
   T1                    Maybe(T2)                 Maybe(T3)

Direct composition: T1 -> Maybe(T3)


6. Multiple Results with List Monad

Using List monad for non-deterministic transformations:

possibleRotations: T1 → List(T1)
    △                    [  △     △     △ ]
   /A\      ------>      [ /A\   /C\   /B\ ]
  / | \                  [/ | \ / | \ / | \]
 /B___C\                 [B___C A___B C___A ]
                         (0°)   (120°) (240°)

allReflections: T1 → List(T1)
    △                    [  △     △     △ ]
   /A\      ------>      [ /A\   /B\   /C\ ]
  / | \                  [/ | \ / | \ / | \]
 /B___C\                 [B___C A___C A___B ]
                         (across different axes)

Kleisli composition:
possibleRotations >=> allReflections: T1 → List(T1)
Produces: all combinations of rotations followed by reflections

7. Identity and Associativity in Kleisli Categories

Identity Kleisli arrow:

return/pure: T1 -> Maybe(T1)
return triangle = Some(triangle)

    △                    ⟨  △  ⟩
   /A\      return       ⟨ /A\ ⟩
  / | \    --------->    ⟨/ | \⟩
 /B___C\                 ⟨B___C⟩

For any Kleisli arrow f: T1 -> Maybe(T2):
f >=> return = f = return >=> f


Associativity

For Kleisli arrows:
- f: T1 → Maybe(T2) (attemptShear)
- g: T2 → Maybe(T3) (tryRotate)
- h: T3 → Maybe(T4) (validateScale)

Associativity: (f >=> g) >=> h = f >=> (g >=> h)

Both paths produce the same T1 → Maybe(T4) transformation:

Path 1: ((attemptShear >=> tryRotate) >=> validateScale)
Path 2: (attemptShear >=> (tryRotate >=> validateScale))

    △
   / \  f    ⟨shape⟩  g    ⟨shape⟩    h    ⟨shape⟩
  /   \ ---> ⟨  or  ⟩ ---> ⟨  or  ⟩ ---->  ⟨  or ⟩
 /_____\     ⟨Nothing⟩     ⟨Nothing⟩       ⟨Nothing⟩
   T1         Maybe(T2)     Maybe(T3)       Maybe(T4)

Visualizing Kleisli categories #

The original category C has:

  • Objects: A, B, C
  • Morphisms: f: A -> B, g: B -> C

The Kleisli category Kl(M) for monad M has:

  • Objects: Same as C: A, B, C
  • Morphisms: Kleisli arrows A -> M B, B -> M C
Original Category C:              Kleisli Category Kl(M):
Objects: A, B, C          -->     Objects: A, B, C
Morphisms: f, g           -->     Morphisms: A -> M(B), B -> M(C)

A ------f------> B                A ------f------> M(B)
        |                                 |
        |                                 | (via flatMap/bind)
        g                                 |
        |                                 v
        v                                M(C)
        C

Composition:                      Kleisli Composition:
g ∘ f : A -> C                   (B -> M(C)) <=< (A -> M(B)) : A -> M(C)

Regular function composition    g ∘ f
becomes Kleisli composition     g <=< f
using the monad's               bind/flatMap

1. Step by step:

a. Apply first Kleisli arrow

A ---------> M(B)
     f

b. Apply flatMap with second Kleisli arrow

M(B) ----flatMap(g)----> M(C)
     where g: B -> M(C)


Combined: Kleisli composition
A ---------> M(C)
   g <=< f


2. Maybe monad:

Original Category (Set):          Kleisli Category Kl(Maybe):

String ----parseInt----> Int      String ----parseInt----> Maybe(Int)
       |                                    |
       |                                    | flatMap
   toString                                 |
       |                                    v
       v                                 Maybe(String)
     String

Regular composition:              Kleisli composition:
toString ∘ parseInt               pureToString <=< parseInt
(partial if parsing fails)        (safely handles failures)

where pureToString: Int -> Maybe(String)


3. The Category Laws

a. Identity return <=< f = f = f <=< return

   A ----f----> M(B)     =     A ----f----> M(B)
      \                            ^
       \                          /
        return                return
         \                      /
          v                    /
         M(A) ----f----> M(B)


b. Associativity: (h <=< g) <=< f = h <=< (g <=< f)

   A --f--> M(B) --flatMap(g)--> M(C) --flatMap(h)--> M(D)

   Both paths yield the same result:
   A ---------> M(D)

CSV Transform Example #

New look for our CSV transform pipeline example. Let's revisit it with fresh eyes - this time seeing it as Kleisli composition in the Kleisli category.

What we thought were "monadic operations" were actually morphisms in a category:

  • parseAge: String -> Maybe Int - Kleisli arrow
  • validateAge: Int -> Maybe Int - Kleisli arrow
  • parseEmail: String -> Maybe String - Kleisli arrow
  • transformCSVRow: [String] -> Maybe Person - Kleisli arrow over a whole row

Conclusion #

Kleisli categories reveal the structure underlying everyday functional programming patterns. What initially appears as "chaining monadic operations" is actually composition of morphisms in a category.

  • Every Monad Gives Rise to a Kleisli Category

  • Composition Becomes Natural: Instead of manually threading effects through code, Kleisli composition provides:

    • Automatic effect handling: Failures, multiple results, and side effects propagate correctly
    • Associative composition: (f >=> g) >=> h = f >=> (g >=> h) ensures predictable behavior
    • Identity preservation: return >=> f = f = f >=> return

Understanding Kleisli categories transforms how we approach problems:

  • Before: "I need to chain these operations and handle the Maybe/Either/IO effects manually"
  • After: "I'm composing morphisms in the Kleisli category - the structure that guarantees correct composition"

This shift from imperative effect-handling to declarative composition leads to:

  • More reliable code (category laws ensure correctness)
  • Better composability (Kleisli arrows compose naturally)
  • Clearer intent (the structure reveals the pattern)
  • Easier testing (pure functions compose into pure pipelines)

Instead of "chaining functions", we are composing morphisms in a category that automatically handles the computational effects, ensures associativity, and provides identity elements.

Source code #

Reference implementation (opens in a new tab)

References

  1. Kleisli category (opens in a new tab)
  2. Arrow tutorial (opens in a new tab)
  3. Functional Pearl: Kleisli arrows of outrageous fortune (opens in a new tab)