Arun Pandian M

Arun Pandian M

Android Dev | Full-Stack & AI Learner

When Composition Breaks: Why We Need Kleisli Category

In the previous posts, we built something beautiful.

We learned:

  • How types give structure.
  • How pure functions give predictability.
  • How composition lets small functions grow into larger systems.
  • We saw that:

    f : A → B
    g : B → C
    g ∘ f : A → C

    Composition was elegant.Associative. Predictable.

    It felt like Lego blocks snapping together. But then reality showed up. And composition broke.

    What We’ve Built So Far

    So far in this series, we learned:

  • Types define boundaries.
  • Pure functions guarantee referential transparency.
  • Composition lets us build pipelines safely.
  • Example:

      val result = transform(fetch(validate(input)))
    

    Beautiful. No mutation. No surprises. Just structure.

    But only because all functions were pure:

    A → B

    Now let’s add reality.

    The Moment Side Effects Enter

    Real software doesn’t live in a math notebook.

    It:

  • Reads databases
  • Makes network calls
  • Fails
  • Logs
  • Retries
  • Caches
  • Depends on time
  • Now our functions look like:

    A → Result<B>
    A → Flow<B>
    A → B?

    And suddenly… Normal composition no longer works.

    Same Use Case (Imperative Style)

    Let’s reuse one realistic scenario:

    Load user → Fetch profile → Cache → Return UI model

    Imperative version:

    fun loadUser(id: String) {
        val user = validate(id)
        if (user == null) {
            showError("Invalid ID")
            return
        }
    
        val profile = api.fetchProfile(user)
        if (!profile.success) {
            showError("Network error")
            return
        }
    
        cache.save(profile.data)
        showProfile(profile.data)
    }

    This works.

    But look carefully. We are not composing functions anymore.

    We are:

  • Calling
  • Inspecting
  • Branching
  • Controlling execution manually
  • Composition has been replaced by control flow.

    What Goes Wrong Structurally

    Imperative handling of side effects leads to:

    Nested control flow: Each new effect adds more branching.

    Manual propagation: You must manually forward failures.

    Scattered concerns:Business logic + error handling + caching are mixed.

    Fragile scaling: Add retry? Add logging? Add analytics? Code grows sideways. The problem is not the syntax. The problem is: Composition is gone.

    First Evolution: Put Effects in Types

    Instead of mutating or branching manually, we change the return type.

    fun validate(id: String): Result<User>
    fun fetchProfile(user: User): Result<Profile>
    fun cache(profile: Profile): Result<Profile>

    Now each function explicitly carries context. But we still need composition.

    The Structural Problem

    Given:

    f : A → Result<B>
    g : B → Result<C>

    Normal composition expects:

    B → C

    But we have:

    B → Result<C>

    So composition breaks. This is the real mathematical issue.

    Second Evolution: Redefine Composition

    Instead of manually unwrapping:

    val r1 = f(a)
    if (r1.isFailure) return r1
    val r2 = g(r1.getOrThrow())
    return r2

    We define a new rule:

    { a -> f(a).flatMap(g) }

    Now composition works again. But not in the original category. In a new one.

    Enter Kleisli Category

    https://storage.googleapis.com/lambdabricks-cd393.firebasestorage.app/kleisli.svg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=firebase-adminsdk-fbsvc%40lambdabricks-cd393.iam.gserviceaccount.com%2F20260225%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20260225T014819Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&X-Goog-Signature=57c85fbf5b88f06ffdd9c395f6221874daf8d14843c7b59acb02594ebb6ea114dcc0826730301e38af5233b8689658b7d4a245e34aa3523d69af32f3bb151481656c7bcf9c16e79161be4c9386297d2e9ace5f6f5219cee21120afd40a20a1cbd40ad907133c8074ed81ca6c0369cf83124abe5593156f24e6b67ae53c69bbf7e4040752131d364ae90f6c037a77154dfff0cedb43700f0fa68b990ca92a457ebb879fba0f45f765fbd2682fccd6db3b42e7ef52f8f2ef31c28b9627ab219fc9b4b01d5de02924938b5faf92ba0e2914ce386bd219e788629b0a0ebdd9eb802e466c589b3692d8a1b20b596aaa105948df0a58813c3c94e5cf134080cbee854a

    A Kleisli category does something radical:

  • Keeps the same objects (types)
  • Changes arrows to:
  • A → M<B>
  • Redefines composition using flatMap
  • Formally:

     g ⋆ f = λa → f(a).flatMap(g)
    

    Now composition survives side effects.

    Imperative vs Kleisli (Same Use Case)

    Imperative

    val user = validate(id)
    if (user == null) return error()
    
    val profile = fetch(user)
    if (!profile.success) return error()
    
    cache(profile.data)

    Manual inspection everywhere.

    Kleisli

    validate(id)
        .flatMap(::fetchProfile)
        .flatMap(::cache)

    No branching. No manual error checks. Composition handles propagation.

    Comparison Summary

    In imperative programming, you constantly check and react. You call a function, inspect the result, and decide what to do next. If something fails, you manually stop the flow. If it succeeds, you move forward. As more side effects appear — errors, logging, retries — your code fills with conditions and branching.

    Kleisli-style programming works differently. You don’t manually inspect every step. Instead, you describe how transformations connect, and the structure takes care of propagating failures or context automatically. The code becomes a straight pipeline instead of a staircase full of “if” statements. What used to require control logic now becomes simple composition.

    Pros of Kleisli

  • Restores composition
  • Encodes effects in types

    Reduces manual branching

    Easier to test

  • Safer concurrency
  • Scales better
  • Cons

  • Requires understanding abstraction
  • Less intuitive at first
  • Adds type complexity
  • Debugging may feel indirect
  • The Deeper Realization

    Imperative programming asks:

    What happens next?

    Kleisli programming asks:

    How do transformations compose?

    That shift changes architecture.

    #FunctionalProgramming#BuildInPublic#CategoryTheory#ProgrammingConcepts#EngineeringMindset#KotlinFP#SoftwareDesign#DeclarativeProgramming#TechDeepDive#KleisliCategory#Monads#FlatMap#ComposableSystems#TypeDrivenDevelopment#EffectHandling#Kleisli