Arun Pandian M

Arun Pandian M

Android Dev | Full-Stack & AI Learner

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:

  • logs
  • metrics
  • traces
  • audit information
  • 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.9

    Nice. 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:

  • Every function depends on external mutable state
  • You must pass log everywhere
  • Forgetting to append log becomes a bug
  • Hard to test
  • 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

    https://storage.googleapis.com/lambdabricks-cd393.firebasestorage.app/writer_monad_img.svg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=firebase-adminsdk-fbsvc%40lambdabricks-cd393.iam.gserviceaccount.com%2F20260227%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20260227T215546Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&X-Goog-Signature=19b3341f96eae09aef5e799876af1d5051227ebbe87b30a0456bf90d38296b65f48077fb09d15f87560c00ac0b886621c7c7bf0af8247141ee8ea016dcb72efa7bc2012cfa1a2e49e4c3dba9f5f1125795597320f03276d34f395232b023e8e9bbc0cbe2612c100f53f7aa37795b9b7a35c5c274db96f7b43e50ffdd19d87469e87544c11e11cf93989ceaa048961868ce78fb4a898316fe3f7c52a8e500efea620f16c4a350c789a16e16183936fb1bc6eaaf7482c492102aae5292377b7c1a48552cea83474b787124fbf744d107848153ef7fc095a058fa1867fdf469a7879b50de075659469c120597563d7f6e6227c83a8cc61bbac16e6dd059526baa4e

    A Writer wraps a value together with some “extra information” that can be combined.

    In math terms:

    Writer<A> = (A, W)

    Where:

  • A → actual value
  • W → some log (must be combinable)
  • 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:

  • Does one job
  • Produces its own log
  • Does not mutate anything
  • Still pure.

    The Real Challenge: Composition

    If we want:

    validate → discount → final result

    We 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.log

    That’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 WriterWith Writer
    Mutable loggingImmutable accumulation
    Manual propagationAutomatic propagation
    Side effectsPure functions
    Repeated boilerplateAbstracted 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:

  • Audit trails
  • Validation warnings
  • Metrics
  • Performance traces
  • Domain events
  • Financial transaction history
  • Anything that is:

  • Accumulative
  • Order-sensitive
  • Associative
  • 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.

    #SoftwareDesign#Immutability#CategoryTheory#MonadConcept#FunctionComposition#FunctionalProgramming#FPArchitecture#DeclarativeProgramming#SideEffects#CleanCode#PureFunctions#KotlinFP#ProgrammingConcepts#WriterMonad#LearnInPublic