Monad I

Async/error handling, promises/optionals

Publish at:
 __  __  ___  _   _    _    ____  ____
|  \/  |/ _ \| \ | |  / \  |  _ \/ ___|
| |\/| | | | |  \| | / _ \ | | | \___ \
| |  | | |_| | |\  |/ ___ \| |_| |___) |
|_|  |_|\___/|_| \_/_/   \_\____/|____/

This one is a bit tough to explain in one go. So I'll start with a real problem. Say, we need to:

  1. Fetch user data from a database (might fail)
  2. Validate the user's permissions (might be unauthorized)
  3. Process their request (might contain invalid data)
  4. Save the results (might encounter storage errors)

One way to solve it by using a straightforward approach:

There are two major difficulties with this code.

  1. Synchronous invocation. It assumes only synchronous nature of the underlying services. In general that is not the case. Advantages of the event loop[1] are not taken into account.

  2. Software entropy[2] is very high. Pyramid of doom[3] consisting of if-elsestatements is just one example.

What can we do about it?

For starters, we can reduce and simplify the pyramid by short-circuiting if statements.

Does it help? Mmmm ..., a bit.

Still, this code is going to end up in a bad shape after multiple modifications because of the requirements change. (Change to async services, additional services checks, additional client validations, etc...). We need something more powerful to solve this code maintainability. But what can we use? Well, here is a quick recap of what we have so far:

  1. Functors - allow us to apply functions to values wrapped in a context
    • map: (a -> b) -> F<a> -> F<b>
  2. Applicative Functors - extend functors to handle functions that are also wrapped in a context
    • apply: F<a -> b> -> F<a> -> F<b>
  3. Monoids - combine values with an associative operation and an identity element
    • combine: (a, a) -> a with identity empty: a

Seems like we are missing something.

What we have is a problem of sequencing computations


Fetch user data ----
        |          |
        | <---------
        ▼
Validate the user's permissions ---
        |                         |
        | <------------------------
        |
        ▼
Process their request ----
        |                |
        |<----------------
        ▼
Save the results

Computations have to be executed in a sequence, yet the result of the previous computation directly affects the behavior of the next one.

From our tool box, applicative functors are the closest structure we have seen so far. They combine computations whose structure is known in advance, so each branch is independent of the values produced by the others. What we are missing is something that can carry the context, then choose the next computation from the previous result. This requires a change in how we have been thinking so far. All the constructs we used before were designed to operate within a context. This time, not only do we need to operate within the context, we need to introduce effects to the context under supervised control.

Remember reader applicative? It provides same input r to all computations. That is not enough, but it gives a hook to hang a thought by. We need to be able to collect intermediate computation results and pass them down the sequence.

Writer applicative #

If the intuition is telling you that we are close, that is not by mistake. Just like there is a structure for the reader applicative, there is a related product-shaped structure: writer applicative[6]. While the reader applicative models computations that consume a shared environment, the writer applicative models computations that produce accumulated output alongside their primary result.

Categorically, the Writer W x A (where W is the "log" type and A is the value type) represents a product object in the category. Unlike Reader's exponential structure, Writer uses the cartesian product to pair computations with their accumulated context. The Writer functor W x (-) maps:

  • Objects: A -> W x A (pairs of log and value)
  • Morphisms: f: A -> B maps to id_W × f: W × A -> W × B

The Writer Applicative builds upon this product structure with operations that respect the underlying monoid structure of W:

  1. Pure (Unit/Return): η: Id -> (W x (-))

    • Categorically: η_A: A -> W x A
    • Implementation: η_A(a) = (mempty_W, a) using the monoid identity
    • This lifts values into the product while providing the neutral element for the log
  2. Apply (Monoidal Product): ⊛: (W x (A -> B)) x (W x A) -> W x B

    • Uses the monoid operation ⊕: W x W -> W to combine logs
    • Combined with function application on the value components
    • Implementation: ((w₁, f) ⊛ (w₂, x)) = (w₁ ⊕ w₂, f(x))

The Categorical Insight - Monoidal Structure #

What makes Writer Applicative categorically significant is its demonstration of how monoidal structure supports computational patterns. Essentially, log type W must form a monoid in the category of values:

  • Associativity: (w₁ ⊕ w₂) ⊕ w₃ = w₁ ⊕ (w₂ ⊕ w₃) ensures log combination is well-defined
  • Identity: mempty ⊕ w = w ⊕ mempty = w provides the neutral element for pure computations
  • Compatibility with mapping: fmap changes only the value part of Writer W A; it leaves the accumulated W value alone

Independent Composition from Monoidal Structure #

The Writer Applicative combines independent computations by combining their logs with the monoid operation. If W is not commutative, the order is still meaningful and the applicative instance preserves a fixed left-to-right order. If W is commutative, then w₁ ⊕ w₂ = w₂ ⊕ w₁, so the accumulated log does not depend on order. That commutativity can make parallel evaluation easier to justify, but applicative structure by itself means independent composition.

Product and Reader Shape #

Writer pairs a value with accumulated context, while Reader consumes shared context as input:

  • Writer: W × A
  • Reader: W -> A

In a cartesian closed category, products and function spaces are connected by currying and uncurrying:

  • Hom(W × A, B) ≅ Hom(A, B^W)

We will name the general pattern later. For now, the practical point is enough: Writer carries context alongside the result, while Reader waits for context before producing a result.

Writer Examples #

Monad #

Monads build upon all these concepts to solve a fundamental problem: sequencing computations that produce wrapped values. While functors let us transform wrapped values and applicative functors let us combine wrapped functions with wrapped values, monads let us chain operations where each step produces a new wrapped value using a flatMap.

flatMap (also called bind or >>=) operation:

flatMap: M<a> -> (a -> M<b>) -> M<b>

This allows us to:

  1. Unwrap a value from its context
  2. Apply a function that produces a new wrapped value
  3. Flatten the result to avoid nested wrapping

Hence, monads handle common programming challenges:

  • Error handling: Chain operations that might fail without nested if-checks
  • Asynchronous operations: Sequence async calls without callback hell
  • State management: Thread state through computations
  • Null safety: Work with potentially absent values safely
  • Logging and side effects: Add behavior without cluttering core logic

Formal definition #

A monad in category theory[4] is a triple (M, η, μ) where:

  • M: C -> C is an endofunctor on category C
  • η: Id_C ⟹ M is a natural transformation from the identity functor to the monad functor (called unit or return)
  • μ: M ∘ M ⟹ M is a natural transformation (called multiplication or join) - flattering two layers

Where:

  • Id_C : C -> C - identity functor
  • (M ∘ M)(A) = M(M(A)) - applies M twice

These must satisfy the monad laws

Monad laws #

  • Left Unit Law (Left Identity)

    μ_A ∘ η_{M(A)} = id_{M(A)}

    M(A) ----η_{M(A)}----> M(M(A)) ----μ_A----> M(A)
    |                                              ^
    |                                              |
    +-------------------id_{M(A)}------------------+
    

    This diagram shows that wrapping M(A) with unit η and then flattening with multiplication μ is equivalent to doing nothing (identity). The composition μ ∘ η acts as the identity on monadic values.

  • Right Unit Law (Right Identity)

    μ_A ∘ M(η_A) = id_{M(A)}

    M(A) ----M(η_A)----> M(M(A)) ----μ_A----> M(A)
    |                                              ^
    |                                              |
    +-------------------id_{M(A)}------------------+
    

    This diagram shows that applying the functor M to unit η and then flattening with multiplication μ is also equivalent to identity. The composition μ ∘ M(η) acts as the identity on monadic values.

  • Associativity Law

    μ_A ∘ M(μ_A) = μ_A ∘ μ_{M(A)}

    M(M(M(A))) ----M(μ_A)----> M(M(A)) ----μ_A----> M(A)
        |                                              ^
        |                                              |
        |          μ_{M(A)}                            |
        v                                              |
      M(M(A)) -------------------------μ_A--------------+
    

    This diagram shows that when flattening a triple-nested monad M(M(M(A))), it doesn't matter whether we flatten the inner layers first (M(μ_A) then μ_A) or the outer layers first (μ_{M(A)} then μ_A). Both paths yield the same result.

Programming Definition #

Let's build up the full definition step by step.

  • Start with a type constructor

    First, we need a way to wrap values in a computational context:

  • Add a way to put values into the context

    Next, we need a function to lift ordinary values into our wrapped type:

    Raw values like 5 or "hello" don't carry information about potential failure, async operations, or other computational effects. The type constructor adds the context.

  • Chain operations that return wrapped values

    We need to chain functions where each function takes an unwrapped value but returns a wrapped value:

    The bind operation >>= solves this by:

    • Checking the context (is the value present? did the computation succeed?)
    • Extracting the value if the context is valid
    • Applying the function to get a new wrapped result
    • Returning the result without double-wrapping
  • See how operations work together

    Now we can chain operations smoothly:

Complete Monad Definition #

A monad in programming[5] requires following three components working together:

  1. Type Constructor M<A>: Wraps values in computational context
  2. Unit/Return pure: A -> M<A>: Lifts values into the context
  3. Bind flatMap: M<A> -> (A -> M<B>) -> M<B>: Chains context-aware operations

It's the bind that fuels everything:

  • Handle the context automatically (checking for null, errors, async completion, etc.)
  • Extract values safely for the next operation
  • Prevent double-wrapping by flattening nested contexts

The multiplication μ and bind >>= are related by:

  • μ_A = join = (>>= id)
  • m >>= f = μ_B (fmap f m) where fmap is the functor operation of M

Monad Laws in Programming Terms #

  • Left Identity

    Wrapping a value and then binding a function should be the same as just applying the function

  • Right Identity

    Binding return/pure to a monadic value should leave it unchanged

  • Associativity

    The grouping of binding operations should not matter

Monadic Do-Notation #

Some languages provide syntactic sugar for monadic compositions (do-notation):

-- Instead of: m1 >>= \x -> m2 >>= \y -> return (f x y)
do x <- m1
   y <- m2
   return (f x y)

While TypeScript doesn't have built-in do-notation, we can simulate it using generator functions and async/await patterns:

C# has LINQ query syntax which provides a natural do-notation-like experience through SelectMany:

This notation makes sequential monadic computations read like imperative code while maintaining all properties/laws of the monad.

Category of Endofunctors #

"Wait, what? Why are we suddenly talking about categories again? Have not we explored categories in every single possible way that was sufficient to build a foundation?

It's a fair question. The answer is "not yet". What is about to happen looks like this.

Imagine you're at a magic show. The magician has been pulling rabbits out of hats, making coins disappear, and doing all sorts of impressive tricks. Then suddenly, they start explaining the physics of electromagnetic fields. You're thinking, "Dude, I just wanted to see some magic tricks!"

Well, monads ARE the magic trick, and the category of endofunctors is the secret mechanism behind the curtain. It's like finding out that your favorite superhero's powers actually come from a very sophisticated piece of alien technology that operates on principles of advanced theoretical physics.

So why do we care?

Once you see what monads really are, a whole bunch of mysterious monad behaviors suddenly make perfect sense.

We have seen how

The collection of all endofunctors on a category C forms its own category, denoted End(C) or [C, C].

and that category is a monoidal one. Now, let's take an opposite look at the monoid. This time from the monoidal category point of view:

A monoid object in the monoidal category End(C) consists of:

  1. Monoid Object: An endofunctor M: C -> C
  2. Multiplication: A natural transformation μ: M ∘ M ⟹ M
    • For each object A in C, we have μ_A: M(M(A)) -> M(A). This is a natural transformation from the functor composition M ∘ M to M
    • Computationally, this flattens nested monadic contexts
    • Naturality condition: μ_B ∘ M(M(f)) = M(f) ∘ μ_A for any f: A -> B
  3. Unit: A natural transformation η: Id_C ⟹ M
    • For each object A in C, we have η_A: A -> M(A). Categorically, this is a natural transformation from the identity functor to M. Computationally, this lifts values into the monadic context
    • Naturality condition: M(f) ∘ η_A = η_B ∘ f for any f: A -> B

Satisfying the monoid laws:

  • Associativity: μ ∘ (μ * Id_M) = μ ∘ (Id_M * μ)
  • Left Unit: μ ∘ (η * Id_M) = Id_M
  • Right Unit: μ ∘ (Id_M * η) = Id_M

Can you see it? This is exactly the monad definition.

Monoid object in End(C) Monad on C
Object of the monoidal category Endofunctor M: C -> C
Monoidal product Nested context M ∘ M, meaning M(M(A))
Unit object Id_C Plain values in C, unchanged by Id_C
Unit map η: Id_C ⟹ M η_A: A -> M(A), also called return or pure
Multiplication map μ μ_A: M(M(A)) -> M(A), or join
Monoid laws Monad laws: left unit, right unit, and associativity

Think of it this way: You know how a regular monoid is just "a thing that you can combine with itself, and there's a boring 'do nothing' version"? Like how you can add numbers (combination) and zero is the "do nothing" element?

Well, endofunctors can be combined too (by composition), and there's an identity endofunctor that does nothing. So endofunctors form their own little mathematical society with its own rules and operations. And monads? They're just the VIP members of this society - the ones with extra special powers (unit and multiplication operations) that make them particularly useful for sequencing computations.

If functors are like different paintbrushes, then the category of endofunctors is like the entire art studio where all the brushes live, complete with rules about how brushes can be combined and transformed. Monads are those fancy, Swiss Army knife brushes that not only paint but also have built-in erasers and color blenders.

A monad is a monoid object in the monoidal category of endofunctors.

Conclusion #

Monads extend the abstractions we have built so far by adding sequencing. A functor maps a function over a value in context. An applicative combines independent contextual values. A monad lets one contextual computation choose the next contextual computation from the value it produced.

That is the key difference:

  • Applicative: the shape of the computation is known in advance.
  • Monad: the next step can depend on the previous result.

In practical terms, this gives us a disciplined way to model error handling, optional values, async workflows, state, logging, and other effects without losing composability.

Source code #

Reference implementation (opens in a new tab)

References

  1. Event loop (opens in a new tab) · Back
  2. Software rot (opens in a new tab) · Back
  3. Pyramid of doom (opens in a new tab) · Back
  4. Monad (category theory) (opens in a new tab) · Back
  5. Monad (functional programming) (opens in a new tab) · Back
  6. Deriving writer monad (opens in a new tab) · Back