Monad II

Implementations, visuals, and pipelines

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

The first part built the abstraction: sequencing contextual computations with pure, bind, join, and the monad laws. It is time for implementations and examples.

Monad implementations #

We have seen how to define and implement monads in different languages. But there are more ways to do that also revealing some interesting insights.

Complete Monad Implementations #

Let's see how to implement monads from scratch, showing the three essential components: the type constructor, unit/return, and bind operations.

Haskell has first-class support for monads with the Monad typeclass[1]:

TypeScript doesn't have built-in monads, but we can implement them using classes and interfaces:

C# integrates monads through LINQ's SelectMany method, providing natural do-notation:

Fluent builder pattern #

There is one more pattern that is worth mentioning. The fluent builder pattern[2][3] provides a way to compose monadic operations through method chaining, creating a domain-specific language (DSL) for sequential computations. This pattern wraps monadic values in a builder class that exposes fluent methods like bind, map, and run, allowing developers to express complex monadic workflows in a readable syntax.

At its core, the fluent builder pattern leverages the insight that monads can present a builder or fluent interface for wrapped values. When you call bind on a builder, the return value becomes the new wrapped value that the next method in the chain operates on. This creates a pipeline where each step can transform, validate, or potentially short-circuit the computation.

This approach is particularly valuable in scenarios involving multiple sequential operations that might fail, such as data validation pipelines, API call sequences, or complex computations where each step depends on the previous result.

Applicatives #

As you can see, in Haskell a monad is built on top of an applicative. Writer applicative was just one illustration to prove the point. But there is a deeper connection between monads and applicatives. And to see it properly, we need to explore hidden applicative secrets.

Applicative as Lax Monoidal Functor #

In category theory, an applicative functor is commonly described as a lax monoidal functor with tensorial strength. This means that it is a functor equipped with coherent ways to lift the monoidal unit and combine independent effectful values. In the programming examples here, the monoidal structure is the product of values.

A functor F : C -> D is lax monoidal if it comes equipped with:

  • A unit map: η : I_D -> F(I_C), where I_C and I_D are the unit objects of the monoidal categories.
  • A product map: μ_{A,B} : F(A) ⊗ F(B) -> F(A ⊗ B)

These must satisfy coherence laws with respect to associativity and unit of the monoidal structures.

In Haskell:

  • The monoidal structure is the cartesian product (×).
  • The lax monoidal structure can be expressed with:
    • unit :: F ()
    • pair :: F a -> F b -> F (a, b)
  • The usual applicative operations are equivalent:
    • pure :: a -> F a can be recovered from unit and fmap
    • liftA2 :: (a -> b -> c) -> F a -> F b -> F c recovers the product map
    • (<*>) :: F (a -> b) -> F a -> F b is the function-application form

In this setting, an applicative functor corresponds to a strong lax monoidal endofunctor on (Set, ×, 1).

     F A × F B  --- μ --->  F(A × B)
         |                      |
         |                      |
         v                      v
     F f × F g  =========>  F(f × g)

This means:

  • Start with independent effectful values F A and F B.
  • Combine them into a pair inside the functor F (A × B).
  • Naturality ensures functions f : A -> A' and g : B -> B' behave consistently.

Tensorial Strength #

A strong lax monoidal functor also has strength: a way to let ordinary values interact with effectful ones.

For cartesian products, this has the shape:

strength :: (a, F b) -> F (a, b)

Applicative programming usually exposes the related operations:

(<*>) :: F (a -> b) -> F a -> F b
liftA2 :: (a -> b -> c) -> F a -> F b -> F c

These operations combine independent effectful values. The strength is the categorical ingredient that lets ordinary values and effectful values fit together coherently.

Monads as Stronger Applicatives #

In Haskell and similar programming settings, every monad gives an applicative structure, but not every applicative is a monad. Categorically:

  • Applicatives combine independent effectful values using coherent monoidal structure.

  • Monads add the ability to flatten nested contexts (join :: m (m a) -> m a), enabling sequencing of dependent computations.

  • Applicative: you can combine independent computations:

    liftA2 (+) (Just 3) (Just 4).

  • Monad: you can feed the output of one computation into another:

    do { x <- Just 3; y <- Just 4; return (x+y) }.

Monads extend applicative-style independent composition with sequential composition (>>=), where later computations can depend on earlier results.

Triangle monad example #

Suppose we have a category of triangles where monadic operations enable sequential geometric transformations with state management.

  • Objects - Triangles in the plane with additional state (history, metadata)
  • Morphisms - Geometric transformations that may fail or accumulate state

A monad M on this category of triangles provides:

  • Type Constructor: M(T) wraps a triangle T with computational context (success/failure, transformation history, accumulated state)
  • Unit/Return: η: T → M(T) lifts a plain triangle into the monadic context
  • Bind: (>>=): M(T) → (T → M(T')) → M(T') sequences transformations while managing state and potential failure
  • Join: μ_T: M(M(T)) → M(T) flattens nested transformation contexts

Unlike applicative functors, which combine transformations whose structure is known independently, monads enable sequential dependent transformations where each step can:

  • Inspect the result of the previous transformation
  • Accumulate transformation history (e.g., rotation angles, translation vectors)
  • Short-circuit on failure (e.g., invalid transformations, boundary violations)
  • Pass state between transformations (e.g., coordinate system changes, constraint violations)

Consider a monad TransformM that tracks transformation history and validates geometric constraints:

TransformM(T) = {
  triangle: T,
  history: [Transformation],
  isValid: boolean,
  constraints: ConstraintSet
}

1. Unit Operation (return/pure)

η: Triangle → TransformM(Triangle)

η(T) = TransformM {
  triangle: T,
  history: [],
  isValid: true,
  constraints: defaultConstraints
}

2. Bind Operation (>>=)

(>>=): TransformM(T) → (T → TransformM(T')) → TransformM(T')

m >>= f = case m of
  TransformM { triangle: T, history: H, isValid: false, ... } →
    TransformM { triangle: invalid, history: H, isValid: false, ... }

  TransformM { triangle: T, history: H, isValid: true, constraints: C } →
    let result = f(T)
        newHistory = H ++ [currentTransform]
        constraintCheck = validateConstraints(result.triangle, C)
    in TransformM {
      triangle: result.triangle,
      history: newHistory,
      isValid: result.isValid && constraintCheck,
      constraints: result.constraints
    }


3. Sequential Transformation Pipeline

Initial Triangle T₀
     |
     | η (unit)
     ▼
TransformM(T₀) { triangle: T₀, history: [], isValid: true }
     |
     | >>= rotate(45°)
     ▼
TransformM(T₁) { triangle: T₁, history: [Rotate(45°)], isValid: true }
     |
     | >>= translate(10, 5)
     ▼
TransformM(T₂) { triangle: T₂, history: [Rotate(45°), Translate(10,5)], isValid: true }
     |
     | >>= scale(2.0)
     ▼
TransformM(T₃) { triangle: T₃, history: [Rotate(45°), Translate(10,5), Scale(2.0)], isValid: true }
     |
     | >>= validateBounds
     ▼
TransformM(T₃) { triangle: T₃, history: [...], isValid: checkBounds(T₃) }


4. Error Propagation: If any transformation fails (e.g., results in degenerate triangle),
all subsequent transformations are skipped:


triangle >>= rotate(45°) >>= invalidScale(0) >>= translate(10,5)
                                ↑
                        failure here stops the chain

5. State Accumulation: Each transformation can modify the constraint set or accumulated metadata:

triangle >>= addConstraint(maxArea(100))
         >>= scale(2.0)  -- might fail if area exceeds constraint
         >>= rotate(90°)

6. History Tracking: The complete transformation sequence is preserved for debugging, undo operations, or optimization:

getHistory(finalResult) = [Rotate(45°), Scale(2.0), Translate(10,5), Reflect(xAxis)]

Key Differences from Applicative #

Aspect Applicative Functor Monad
Dependency Independent transformations Sequential dependent transformations
State Fixed structure known up front State can be threaded between steps
Failure Combines independent failures according to the instance Can choose the next step only after success
History Can accumulate independent history Can let history influence later steps
Constraints Static constraints Dynamic constraint evolution

Monadic Laws in Triangle Context #

  1. Left Identity: η(T) >>= f = f(T)

    • Lifting a triangle and applying a transformation equals applying the transformation directly
  2. Right Identity: m >>= η = m

    • Binding with the unit operation preserves the monadic triangle
  3. Associativity: (m >>= f) >>= g = m >>= (\x -> f(x) >>= g)

    • Regrouping the binding transformations does not affect the final result

Visualizing monads #

A monad M builds upon functor and applicative with three essential components:

  1. Type Constructor M - Wraps values in computational context (error handling, state, etc.)
  2. Unit/Return η: A -> M(A) - Lifts pure values into the monadic context
  3. Bind (>>=): M(A) -> (A -> M(B)) -> M(B) - Chains computations that produce wrapped values
1. Sequential dependent computations

Inside one category:
Objects: A, B, C and their wrapped forms M(A), M(B), M(C)
Morphisms: ordinary arrows A -> B, functor-mapped arrows M(A) -> M(B), and arrows A -> M(B)

Sequential Chain:
A ----f----> B ----g----> C

Monadic Chain:
M(A) ----bind(f')----> M(B) ----bind(g')----> M(C)

where:
- f': A -> M(B)  (function that produces wrapped result)
- g': B -> M(C)  (function that produces wrapped result)


2. Dependent vs Independent Computation

Applicative (Independent):

F(A)     F(B)     F(C)
  |        |        |
  |        |        |    Independent computations
  |        |        |    can be combined without depending on one another
  v        v        v
liftA3 h F(A) F(B) F(C) -----> F(result)


Monad (Sequential):

M(A) --bind--> M(B) --bind--> M(C) --bind--> M(result)
  |             |              |              |
  |             |              |              |
  └─── f ───────┴─── g ────────┴─── h ────────┘


3. Monad Laws Visualization

- Left Identity: return a >>= f = f a

     a
     |
     | return/pure
     v
   M(a) ------bind(f)------> M(b)

   equals

     a --------f---------> M(b)

- Right Identity: m >>= return = m


   M(a) ------bind(return)------> M(a)

   equals

   M(a) --------identity--------> M(a)

- Associativity: (m >>= f) >>= g = m >>= (\x -> f x >>= g)

Left side:
M(a) --bind(f)--> M(b) --bind(g)--> M(c)

Right side:
M(a) --bind(\x -> f(x) >>= g)--> M(c)


4. Monadic Context Flow

Pure World                 Monadic World

    A                        M(A)
    |                         |
    | computation             | bind
    v                         v
    B                        M(B)
    |                         |
    | computation             | bind
    v                         v
    C                        M(C)

Context preserved and threaded through each step

5. Error Handling Example (Maybe Monad)


Just 10 --bind--> safeDivide(_, 2) --bind--> Just 5 --bind--> multiplyBy(_, 3) --> Just 15

Just 10 --bind--> safeDivide(_, 0) --bind--> Nothing ----bind----> Nothing --> Nothing
                                      |                     |
                                      |                     |
                               Short-circuits         Skips computation

6. State Threading Example

Initial State: S₀

computation₁: A -> State S₀ (B, S₁)
              |              |
              v              v
           M(A,S₀) -----> M(B,S₁)
                           |
                           | bind with computation₂
                           v
                      M(C,S₂) -----> M(D,S₃)
                                      |
                                      | final result
                                      v
                                   (D, S₃)

State is automatically threaded through the computation chain

Comparison Summary #

Aspect Functor Applicative Monad
Operation map: (A -> B) -> F(A) -> F(B) apply: F(A -> B) -> F(A) -> F(B) bind: M(A) -> (A -> M(B)) -> M(B)
Dependency Transform values Independent effects Sequential dependencies
Context Preserve context Combine contexts Transform context
Use Case Simple transformations Independent validation/combination Error chains, state

Initial problem #

Finally we can get back to out original user request problem. And this is how the solution looks like now (feel the difference):

CSV Transform example #

Our CSV transformation pipeline benefits from monadic composition as well.

The monadic approach transforms our transformation pipeline from independent operations to a dependent chain where:

  • 🔗 Sequential Processing: Each transformation receives the result of the previous step
  • 🚨 Early Termination: Invalid data immediately stops the pipeline
  • 📝 State Tracking: Accumulate transformation history and validation results
  • 🎯 Contextual Logic: Later transformations can inspect earlier results

Conclusion #

The true power of monads becomes apparent when we see how they solve real-world problems:

  • Error Propagation: Instead of littering code with null checks and error handling, monads like Maybe and Either automatically manage failure paths
  • Sequential Dependencies: Unlike applicative functors that handle independent operations, monads excel at computations where each step depends on previous results
  • Context Preservation: Whether it's state, configuration, or accumulated logs, monads seamlessly thread context through complex computations
  • Composability: Monadic operations compose naturally, allowing complex workflows to be built from simple, reusable pieces

Practical Takeaways #

  1. Start Simple: Begin with Maybe and Either to handle null values and errors elegantly
  2. Think Sequentially: When you need computations that depend on previous results, consider monads over applicative functors
  3. Embrace Composition: Design small, focused monadic operations that compose into larger workflows
  4. Use Language Features: Leverage your language's monadic support—LINQ in C#, async/await patterns, or custom implementations
  5. Don't Overuse: Monads are powerful tools, but not every problem needs a monadic solution

Common Monads #

  • Maybe/Option: Handles null values
  • Either/Result: Handles errors
  • List: Non-deterministic computations
  • IO: Side effects and input/output
  • State: Stateful computations
  • Reader: Environment/context passing
  • Writer: Logging and accumulation
  • Cont: Continuations and control flow

Monads provide a principled way to handle the messy realities of computation—errors, state, dependencies, and side effects—while maintaining the benefits of functional programming: composability, reasoning, and modularity. Whether you're handling user authentication flows, processing data transformations, or managing complex state transitions, monadic patterns provide a structured approach to sequential computations.

The next time you find yourself writing nested conditionals, chaining nullable operations, or manually managing error states, remember: there's probably a monad for that.

Source code #

Reference implementation (opens in a new tab)

References

  1. All About Monads (opens in a new tab) · Back
  2. Builder pattern (opens in a new tab) · Back
  3. Fluent interface (opens in a new tab) · Back