Writer Monad — When Functions Need to Talk Without Breaking Purity
One of the first things you learn in functional programming is:
Keep functions pure.
A function should take an input and return an output — nothing more. But real systems need more than just results.
They need:
So how do functions produce extra information without breaking purity?
This is exactly the problem the Writer Monad solves.
I Just Want to Add Logging…
You start with something clean.
fun validateAmount(amount: Double): Boolean =
amount > 0
fun applyDiscount(amount: Double): Double =
amount * 0.9Nice. Pure. Predictable.
Now product says:
“We need logs for auditing. Every step must record what happened.”
So you change it.
fun validateAmount(amount: Double, log: MutableList<String>): Boolean {
log.add("Validating amount")
return amount > 0
}
fun applyDiscount(amount: Double, log: MutableList<String>): Double {
log.add("Applying discount")
return amount * 0.9
}Now:
Not pure anymore
This is the classic side-effect pollution problem.
The Idea Behind Writer
What if:
Instead of mutating logs, each function returns its own result and a small piece of log.
Then logs can be combined later. That’s the Writer idea.
The Core Concept
A Writer wraps a value together with some “extra information” that can be combined.
In math terms:
Writer<A> = (A, W)
Where:
In Kotlin:
data class Writer<A>(
val value: A,
val log: List<String>
)Pure but Embellished Functions
Now rewrite functions to return Writer.
fun validateAmount(amount: Double): Writer<Boolean> =
Writer(
value = amount > 0,
log = listOf("Validated amount: $amount")
)
fun applyDiscount(amount: Double): Writer<Double> =
Writer(
value = amount * 0.9,
log = listOf("Applied 10% discount")
)Each function:
Still pure.
The Real Challenge: Composition
If we want:
validate → discount → final resultWe must combine logs.
Manual way (ugly):
fun process(amount: Double): Writer<Double> {
val v = validateAmount(amount)
if (!v.value) return Writer(0.0, v.log)
val d = applyDiscount(amount)
return Writer(
value = d.value,
log = v.log + d.log
)
}Notice repetition:
log = v.log + d.logThat’s composition logic leaking everywhere. We need to abstract that.
Enter flatMap (The Writer Monad)
Add this to Writer:
fun <B> Writer<A>.flatMap(f: (A) -> Writer<B>): Writer<B> {
val next = f(this.value)
return Writer(
value = next.value,
log = this.log + next.log
)
}This is the magic.
It says:
Apply next function, and automatically merge logs.
Clean Composition
Now process becomes:
fun process(amount: Double): Writer<Double> =
validateAmount(amount)
.flatMap { isValid ->
if (!isValid)
Writer(0.0, listOf("Invalid amount"))
else
applyDiscount(amount)
}No manual log merging. No mutation. Fully composable.
What Writer Monad Solves
| Without Writer | With Writer |
|---|---|
| Mutable logging | Immutable accumulation |
| Manual propagation | Automatic propagation |
| Side effects | Pure functions |
| Repeated boilerplate | Abstracted composition |
The Mathematical Insight
Writer works because logs form a Monoid.
A monoid means:
1. There is an identity element - For List → emptyList()
2. There is an associative combine operation - For List → +
That’s why logs can accumulate safely.
Writer is:
A value + a monoidal context.
Real-World Uses Beyond Logging
Writer is not just logging.
It can accumulate:
Anything that is:
Final Takeaway
Writer Monad is not about logging.
It’s about:
Keeping your functions pure While allowing them to accumulate context
Instead of mutating global state, we carry context along safely and compose it mathematically.
