Skip to content

Monade - Mathematical Explanation

Core Concepts

Monad M

A monad is a type constructor M with two operations:

  • return (we call it of): a → M a
  • bind (we call it chain): M a → (a → M b) → M b

Kleisli Arrow

A Kleisli arrow is a function of type a → M b where M is a monad.


API Operations

1. of / pof - Monad Constructor

of : a → M a
pof : a → P a  (where P is the async/Promise monad)
Example:
of(5)    // Wraps 5 in a monad: M(5)
pof(5)   // Wraps 5 in an async monad: P(5)

Mathematical meaning: The return operation that lifts a pure value into the monad.

Difference between of and pof:
  • of creates a synchronous monad
  • pof creates an asynchronous monad (Promise-based), allowing async operations in the chain

2. map - Functor Map

map : (a → b) → M a → M b
Example:
of(5).map(x => x * 2)  // M(5) → M(10)
pof(5).map(x => x * 2)  // P(5) → P(10)
Mathematical composition:
map f = chain (of ∘ f)

This is why we can chain maps: each map preserves the monadic structure.


3. tap - Side Effect

tap : (a → ()) → M a → M a
Example:
of(5).tap(x => console.log(x))  // M(5), logs 5, returns M(5)

Mathematical meaning: Performs side effects without changing the wrapped value. It's equivalent to:

tap f m = m >>= λx. (f x; return x)

4. chain - Monadic Bind

chain : (a → M b) → M a → M b
Example:
of(5).chain(x => of(x * 2))  // M(5) → M(10)

Mathematical meaning: This is the monadic bind (>>=) operation. It's associative:

(m >>= f) >>= g ≡ m >>= (λx. f x >>= g)

5. val - Extract Value

val : M a → a
val : P a → Promise a  (for async monads)
Example:
of(5).val()        // M(5) → 5
await pof(5).val() // P(5) → Promise(5) → 5

Mathematical meaning: Extracts the wrapped value. Note: This makes our monad "pointed" (not all monads have this).


6. fn - Extract Kleisli Arrow Function

fn : Arrow a b → (a → M b)
Example:
const arrow = ka()
  .map(x => x * 2)
  .map(x => x + 10)
 
const f = arrow.fn()  // Returns the composed function: a → M b
f(5)                  // M(20) - applies the arrow to 5

Mathematical meaning: The fn() method extracts the underlying Kleisli arrow (the composed function) from the arrow builder object. This is necessary because ka() and pka() return a builder object with chainable methods, not the function itself.

Why it exists: The arrow builders (ka/pka) create a fluent interface for composing functions. The fn() method "finalizes" the composition and returns the actual Kleisli arrow that can be applied to values:

ka() : () → ArrowBuilder
ArrowBuilder.fn() : () → (a → M b)

Kleisli Arrows (ka/pka)

Construction

ka : () → (a → M a)   // Synchronous Kleisli arrow builder
pka : () → (a → P a)  // Asynchronous Kleisli arrow builder

Operations on Arrows

map : (b → c) → (a → M b) → (a → M c)
chain : (b → M c) → (a → M b) → (a → M c)
fn : Arrow a b → (a → M b)
Example with ka (synchronous):
const syncArrow = ka()
  .map(x => x * 2)    // Creates: a → M(2a)
  .map(x => x + 10)   // Creates: a → M(2a + 10)
  .fn()               // Converts to usable function
 
syncArrow(5).val()    // 20
Example with pka (asynchronous):
const asyncArrow = pka()
  .map(x => x * 2)
  .map(async x => {
    await delay(100)
    return x + 10
  })
  .fn()
 
await asyncArrow(5).val()  // 20 (after delay)

Key difference: pka handles async operations and returns P monads (Promise-based), while ka is purely synchronous and returns M monads.


Devices (dev/pdev) - Domain-Specific Monads

What are Devices?

Devices are specialized monads that can be extended with custom methods. They maintain all monad operations while adding domain-specific functionality.

Construction

dev : (Maps, Tos) → (a → Device a)   // Synchronous device
pdev : (Maps, Tos) → (a → PDevice a) // Asynchronous device

Where:

  • Maps: Object of chainable methods that transform and return a new device
  • Tos: Object of terminal methods that extract/compute final values

Example with dev (synchronous):

// Define custom string manipulation device
const stringDevice = dev(
  {
    // Maps: chainable transformers
    upper: (str) => str.toUpperCase(),
    repeat: (str, n) => str.repeat(n),
    trim: (str) => str.trim()
  },
  {
    // Tos: terminal operations
    length: (str) => str.length,
    startsWith: (str, prefix) => str.startsWith(prefix)
  }
)
 
// Usage
const result = stringDevice("  hello  ")
  .trim()           // Device("hello")
  .upper()          // Device("HELLO")
  .repeat(2)        // Device("HELLOHELLO")
  .length()         // 10
 
// Can still use monad operations
stringDevice("test")
  .map(s => s + "!")     // Device("test!")
  .upper()               // Device("TEST!")
  .val()                 // "TEST!"

Example with pdev (asynchronous):

// Define async HTTP device
const httpDevice = pdev(
  {
    // Async maps
    get: async (url) => await fetch(url),
    json: async (response) => await response.json(),
    filter: async (data, predicate) => data.filter(predicate)
  },
  {
    // Async tos
    count: async (data) => data.length,
    save: async (data, filename) => await saveToFile(data, filename)
  }
)
 
// Usage
const users = await httpDevice("https://api.example.com/users")
  .get()                              // PDevice(Response)
  .json()                             // PDevice(users[])
  .filter(user => user.active)       // PDevice(activeUsers[])
  .count()                            // 42

Mathematical Properties of Devices

Devices maintain the monad laws while adding custom operations:

  1. Monad laws still apply to map, chain, tap
  2. Custom maps are functorial: device.customMap(f).customMap(g) ≡ device.customMap(g ∘ f)
  3. Conversion: device.monad() returns the underlying monad (M or P)

Option Handling (opt/popt)

Purpose

Safe error handling that converts failing monads to null values.

opt : M a → M (a | null)    // Synchronous option
popt : P a → P (a | null)   // Asynchronous option
Example:
// Synchronous
const safe = opt(of(5).map(x => x / 0))  // M(null) instead of error
 
// Asynchronous
const asyncSafe = await popt(
  pof(url).map(fetch).chain(parseJSON)
)  // P(null) if fetch or parse fails

Why Chaining Works Mathematically

Functor Laws

1. map id = id                    // Identity
2. map (g ∘ f) = map g ∘ map f   // Composition

Monad Laws

1. of(a).chain(f) ≡ f(a)                    // Left identity
2. m.chain(of) ≡ m                          // Right identity  
3. m.chain(f).chain(g) ≡ m.chain(x => f(x).chain(g))  // Associativity

Kleisli Composition

For functions f: a → M b and g: b → M c:

(f >=> g) = λa. f(a).chain(g)

This is associative, which is why we can chain operations cleanly!


Complete Example: Sync vs Async vs Device

// 1. Synchronous monad
const syncResult = of(5)
  .map(x => x * 2)
  .map(x => x + 10)
  .val()  // 20
 
// 2. Asynchronous monad
const asyncResult = await pof(5)
  .map(x => x * 2)
  .map(async x => {
    await delay(100)
    return x + 10
  })
  .val()  // 20
 
// 3. Kleisli arrow (reusable)
const pipeline = ka()
  .map(x => x * 2)
  .map(x => x + 10)
  .fn()
 
pipeline(5).val()  // 20
pipeline(10).val() // 30
 
// 4. Device with custom methods
const mathDevice = dev(
  {
    double: x => x * 2,
    square: x => x * x,
    add: (x, n) => x + n
  },
  {
    isEven: x => x % 2 === 0,
    toString: x => `Result: ${x}`
  }
)
 
mathDevice(5)
  .double()      // Device(10)
  .square()      // Device(100)
  .add(20)       // Device(120)
  .toString()    // "Result: 120"

Key Insights

The beauty of this API is that it:

  1. Maintains mathematical rigor - All monad and functor laws are preserved
  2. Provides practical utilities - Async support, custom devices, safe error handling
  3. Enables composition - Everything composes cleanly through Kleisli arrows
  4. Extends gracefully - Devices allow domain-specific extensions without breaking monad laws
  5. Separates concerns - Builder pattern (ka/pka) separates composition from execution

The entire system is built on solid category theory foundations while remaining intuitive and practical for real-world use!

Libraries