When Composition Breaks: Why We Need Kleisli Category
In the previous posts, we built something beautiful.
We learned:
We saw that:
f : A → B
g : B → C
g ∘ f : A → CComposition 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:
Example:
val result = transform(fetch(validate(input)))
Beautiful. No mutation. No surprises. Just structure.
But only because all functions were pure:
A → BNow let’s add reality.
The Moment Side Effects Enter
Real software doesn’t live in a math notebook.
It:
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:
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 → CBut 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 r2We 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
A Kleisli category does something radical:
A → M<B>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
Encodes effects in types
Reduces manual branching
Easier to test
Cons
The Deeper Realization
Imperative programming asks:
What happens next?
Kleisli programming asks:
How do transformations compose?
That shift changes architecture.
