MonadPlus

Parser combinators, error recovery

Published:
 __  __  ___  _   _    _    ____  ____    ____  _    _   _ ____
|  \/  |/ _ \| \ | |  / \  |  _ \/ ___|  |  _ \| |  | | | / ___|
| |\/| | | | |  \| | / _ \ | | | \___ \  | |_) | |  | | | \___ \
| |  | | |_| | |\  |/ ___ \| |_| |___) | |  __/| |__| |_| |___) |
|_|  |_|\___/|_| \_/_/   \_\____/|____/  |_|   |_____\___/|____/

Alternative[1] gives applicative functors a way to express choice and failure with empty and <|>. MonadPlus[2] carries that same idea into monadic computations, where later steps may depend on earlier results.

MonadPlus is where parsing truly shines. Consider a complex parser that needs to handle multiple syntax variants, backtrack on failure, or implement lookahead: Alternative handles the choice between alternatives, but MonadPlus adds the ability to sequence those choices with early termination when parsing fails.

Think of MonadPlus as Alternative with dependency. Where Alternative can choose between independent computations, MonadPlus can chain dependent computations together while preserving the ability to fail gracefully and try alternatives.

MonadPlus Formal Definition #

MonadPlus combines the structure of a monad with the choice operations of Alternative:

class (Monad m, Alternative m) => MonadPlus m where
  mzero :: m a              -- Identity element (same as empty)
  mplus :: m a -> m a -> m a -- Associative operation (same as <|>)

Laws:

Monoid Laws (inherited from Alternative):

  • Left Identity: mzero `mplus` m = m
  • Right Identity: m `mplus` mzero = m
  • Associativity: (l `mplus` m) `mplus` r = l `mplus` (m `mplus` r)

Monad Laws (inherited from Monad):

  • Left Identity: return a >>= f = f a
  • Right Identity: m >>= return = m
  • Associativity: (m >>= f) >>= g = m >>= (\x -> f x >>= g)

Common MonadPlus laws (interaction between monad and choice):

  • Left Zero: mzero >>= f = mzero
  • Left Distribution: (m `mplus` n) >>= f = (m >>= f) `mplus` (n >>= f)

Some instances also satisfy a right-zero law, m >> mzero = mzero, but this law is not valid for every useful MonadPlus-like structure. The key insight is that mzero acts as a left absorbing element for monadic bind (>>=), meaning that a computation that is already failed will short-circuit and propagate the failure.

From a category theory viewpoint, MonadPlus adds monoid-like choice to monadic computations. This gives us:

MonadPlus Structure:
┌─────────────────┐
│   m a >>= f     │  ─┐
│  (sequencing)   │   │
└─────────────────┘   │
                      ├──> MonadPlus Laws
┌─────────────────┐   │
│   m `mplus` n   │   │
│   (choice)      │  ─┘
└─────────────────┘

Natural Transformation Properties:

The MonadPlus choice operation is natural in the result type and preserves the intended choice semantics:

Haskell

-- Naturality of mplus
fmap f (m `mplus` n) = fmap f m `mplus` fmap f n

-- Monad-Choice interaction
(m `mplus` n) >>= f = (m >>= f) `mplus` (n >>= f)

This naturality ensures that MonadPlus provides a principled way to combine monadic computations with choice, enabling complex patterns like parser combinators, search algorithms, and error recovery mechanisms.

MonadPlus Examples #

A parser with backtracking, showing how MonadPlus enables different parsing strategies with automatic failure handling and choice between different parsing approaches.

Async operation chaining with service fallbacks, demonstrating how MonadPlus patterns help build resilient distributed systems with multiple retry strategies.

Database query system with retry logic, showing how MonadPlus enables robust data access patterns with automatic fallback between different data sources.

MonadFail #

While MonadPlus provides choice and fallback mechanisms for monadic computations, sometimes we need more precise control over why a computation fails. Pattern matching failures, partial functions, and explicit error conditions all require a way to signal failure with meaningful error messages. This is where MonadFail comes into play.

MonadFail separates the concern of explicit failure from the choice operations of MonadPlus. Where MonadPlus focuses on trying alternatives until one succeeds, MonadFail provides a principled way to signal that a computation has failed with a specific reason, enabling better error reporting and debugging.

MonadFail provides a single operation for explicit failure with error messages:

class Monad m => MonadFail m where
  fail :: String -> m a

Laws:

  • Absorption: fail s >>= f = fail s (failure propagates)
  • Left zero: fail s >> m = fail s (failure short-circuits)

The fail function creates a monadic computation that represents failure with an error message. This is particularly useful for pattern match failures and explicit error conditions where you want to provide context about what went wrong.

The motivation for MonadFail stems from how Haskell's do-notation handles refutable pattern matches. Consider what happens when you write a pattern match that may fail:

do (x:xs) <- items
   return (x + sum xs)

This code gets desugared into something more complex:

let handle (x:xs) = return (x + sum xs)
    handle _      = fail "Pattern match failure"
in items >>= handle

The problem becomes clear: a refutable pattern in do-notation requires a fail function. Without an explicit type-class constraint, that failure capability is hidden.

Consider these common monads where fail cannot be sensibly implemented:

  • Either: What error should we put in Left?
  • IO: Should pattern failures throw runtime exceptions?
  • Reader: Should we terminate the entire context-dependent computation?

Before MonadFail separated this behavior, failed pattern matches in do-notation could fall back to error, turning pattern match failures into runtime crashes. This meant that seemingly polymorphic monadic code could suddenly terminate your program:

-- This looks safe and polymorphic...
processItems :: Monad m => m [Int] -> m Int
processItems items = do
  (x:xs) <- items  -- But this pattern match could crash!
  return (x + sum xs)

-- Using with IO: crashes on empty list
result1 <- processItems (return [])  -- Runtime error!

-- Using with Either: also crashes
let result2 = processItems (Right [])  -- Runtime error!

MonadFail solves this by making failure capabilities explicit in the type signature. Now the compiler can track which computations might fail and ensure that fail is only available when the monad can handle it meaningfully:

-- Now the type signature tells the truth
processItems :: MonadFail m => m [Int] -> m Int
processItems items = do
  (x:xs) <- items  -- Pattern match requires MonadFail
  return (x + sum xs)

-- Safe usage with Maybe (has MonadFail instance)
result1 = processItems (Just [])      -- Nothing

-- Safe usage with IO (has MonadFail instance)
result2 <- processItems (return [])   -- IOException

-- Won't compile with plain Either (no MonadFail instance)
-- let result3 = processItems (Right [])  -- Compile error!

This design makes refutable pattern failure explicit: if do-notation needs to handle pattern match failures, the MonadFail m constraint makes that requirement visible in the type.

Visualization #

1. MonadPlus Structure

MonadPlus Pattern:

Monadic Bind + Choice:
m >>= f ──┐
          ├──> m a (sequential composition)
n >>= f ──┘
mplus

Failure Propagation:

mzero >>= f = mzero    (left zero)
m >> mzero = mzero     (right zero, for instances that satisfy it)

Parser Backtracking:

        ┌─ try hexNumber ──> success? ──> return result
input ──┤
        ├─ try binary ────> success? ──> return result
        └─ try decimal ───> success? ──> return result
                            │
                            ▼
                         failure ──> mzero

Left Distribution:

(m `mplus` n) >>= f = (m >>= f) `mplus` (n >>= f)

     m ──┐         m >>= f ──┐
         ├─ mplus            ├─ mplus ──> result
     n ──┘         n >>= f ──┘
         │              │
         ▼              ▼
      >>= f          distribute


2. MonadFail Structure

Explicit Failure:

fail :: String -> m a

Type Safety:

Monad m      => no refutable do-patterns
MonadFail m  => may fail (explicit in type)

Failure Flow:

computation ──> pattern match ──> success? ──┐
                     │                       │
                     ▼                       ▼
                  failure                continue
                     │                       │
                     ▼                       ▼
              fail "message"             more code

Absorption Laws:

fail s >>= f = fail s    (failure propagates)
fail s >> m = fail s     (failure short-circuits)

Before MonadFail (dangerous):

processItems :: Monad m => m [Int] -> m Int
                ├── could crash! ──┤

After MonadFail (safe):

processItems :: MonadFail m => m [Int] -> m Int
                ├── explicit failure capability ──┤


3. Hierarchy Relationships

Type Class Hierarchy:

    Functor
       │
       ▼
   Applicative ──┐
       │         │
       ▼         ▼
     Monad   Alternative
       │         │
       ├─────────┤
       ▼         ▼
   MonadFail  MonadPlus
       │         │
       └────┬────┘
            ▼
      Combined Power:
   - Sequential composition (Monad)
   - Choice operations (Alternative)
   - Explicit failure (MonadFail)
   - Backtracking (MonadPlus)

Capability Matrix #

Type Class Choice Sequence Fail Backtrack
Alternative
Monad
MonadFail
MonadPlus
MonadPlus+Fail

Conclusion #

Programming is fundamentally about making choices. Sometimes we need fallback strategies when our first attempt fails. Sometimes we need to sequence dependent operations while preserving the ability to backtrack. Sometimes we need explicit control over why and how computations fail.

Alternative gives us choice within applicative computations. With empty and <|>, we could express fallback strategies and try multiple approaches until one succeeded. This alone changes parsing, validation, and search algorithms by providing a principled way to handle failure and choice.

MonadPlus elevates these concepts to the monadic level. By combining the sequential composition of monads with the choice operations of Alternative, we gained the power of backtracking - the ability to explore multiple computational paths with full context awareness. This is why MonadPlus shines in complex parsing scenarios, where each parsing decision depends on previous results, yet we still need the ability to backtrack and try alternatives.

MonadFail addressed a subtle but crucial problem: making failure capabilities explicit and safe. By separating explicit failure from general monadic operations, MonadFail ensures that type signatures tell the truth about what can fail and why.

From a category theory standpoint, we've witnessed the emergence of increasingly sophisticated structures:

  • Alternative enriches applicative functors with monoid-like choice for each result type
  • MonadPlus combines monadic sequencing with Alternative-style choice operations
  • MonadFail makes refutable do-pattern failure explicit in the type

As software systems grow more complex and distributed, the need for principled approaches to choice, failure, and recovery only increases. Alternative, MonadPlus, and MonadFail provide a solid foundation for building reliable systems that gracefully handle the inevitable failures and choices that define real-world computing. A system that combines all three - choice operations, sequential composition, explicit failure, and backtracking - can handle complex scenarios with surprisingly simple code. This is the essence of functional programming: finding the right abstractions that make the complex simple, while preserving the flexibility to handle edge cases and unexpected situations.

Source code #

Reference implementation (opens in a new tab)

References

  1. Alternative and MonadPlus (opens in a new tab) · Back
  2. MonadPlus (opens in a new tab) · Back