Free monad

DSL creation, configuration languages

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

After wrestling with comonads and their spatial wizardry — where every computation was imprisoned by its neighborhood context — we now face the ultimate plot twist: Freedom.

In programming, as in life, the word "free" comes with strings attached:

  1. Free as in "Free Beer" 🍺: You get monad structure without choosing how to interpret each operation upfront
  2. Free as in "Freedom" 🕊️: You can choose your interpretation later, like a philosophical choose-your-own-adventure book
  3. Free as in "Free Range" 🐔: Your computations roam the semantic landscape until you decide to pin them down

A monad can also be free. But what does it mean to build one without choosing its effects? Think of it as computational procrastination elevated to an art form. Instead of immediately executing effects, we can defer the decision of how to execute them until we absolutely have to.

A Free Monad is a data structure that looks like a monadic computation but doesn't perform its described effects until you provide an interpreter. It's the programming equivalent of writing a recipe without cooking the meal.

Free Monads solve a fundamental existential crisis in functional programming:

"How can I build complex programs without committing to specific effects until interpretation?"

This creates a separation of concerns:

  • What your program does (the structure)
  • How your program does it (the interpretation)

It's the difference between choreographing a dance and actually dancing it. Free Monads let you perfect the choreography without worrying about whether you'll perform it in a ballroom, a barn, or a zero-gravity chamber.

Great power comes with great... well, you know the rest. Free Monads unlock several opportunities:

  1. 🧪 Testing: Supply test interpretations for the effects your DSL describes without changing your core logic
  2. 🔄 Optimization: Transform computations before executing them
  3. 🎭 Multiple Interpretations: Run the same program in different contexts
  4. 🧩 Modularity: Build programs like LEGO blocks of pure intent

Time to make our programs a lot more philosophical and slightly more confusing, but infinitely more powerful.

Free Monads #

In a nutshell, instead of defining what a monad does, we define what a monad looks like as pure data. We create a structure that captures the shape of monadic computation without committing to any particular interpretation.

Formal Definition #

A Free Monad over a functor f is defined as:

data Free f a
  = Pure a                    -- Pure value (return)
  | Free (f (Free f a))       -- Suspended computation

This recursive definition captures the essence of monadic structure:

  • Pure a represents a pure value wrapped in the monad (equivalent to return or pure)
  • Free (f (Free f a)) represents a suspended computation that can be further composed

The functor f represents your "instruction set" - the basic operations your program can perform. The Free Monad organizes these instructions into a program without executing them.

The Monad Instance #

Free Monads automatically satisfy the monad laws:

instance Functor f => Functor (Free f) where
  fmap g (Pure a) = Pure (g a)
  fmap g (Free layer) = Free (fmap (fmap g) layer)

instance Functor f => Applicative (Free f) where
  pure = Pure
  Pure g <*> x = fmap g x
  Free layer <*> x = Free (fmap (<*> x) layer)

instance Functor f => Monad (Free f) where
  Pure a >>= k = k a
  Free layer >>= k = Free (fmap (>>= k) layer)

Notice how >>= (bind) doesn't actually execute anything - it just builds up more structure. The real magic happens when you provide an interpreter.

Categorical Perspective #

The adjunction example built a free monoid from a set of generators. A function assigning meaning to each generator extended uniquely to a monoid homomorphism on lists. Here the generators are operations described by a functor f, and the free construction gives them monadic sequencing.

The free-monad construction is left adjoint to the forgetful functor U, which maps a monad to its underlying functor.

Here is what that means for interpretation. For any monad m, a natural transformation α : f → U(m) assigns a meaning in m to each operation. The adjunction extends α uniquely to a monad homomorphism φ : Free f → m, which interprets the whole program. Its universal property is captured by this commuting diagram:

    f -------- α --------> U(m)
     \                    ↑
    η \                   │ U(φ)
       ↓                  │
      U(Free f) ──────────┘

The unit η : f → U(Free f) lifts one operation into a program. The counit Free (U(m)) → m interprets a program whose operations already belong to m. The diagram says α = U(φ) ∘ η: interpreting a lifted operation agrees with interpreting that operation directly. Choosing a different α gives the same program a different interpreter, as the example below shows.

Example: Console DSL #

We will describe console operations once, build programs from them, and then interpret the same programs in several ways.

Step 1: Define Your Operations (Functor) #

First, we define the basic operations our DSL supports:

The ConsoleF functor defines our "instruction set":

  • WriteLine: Output a string and continue
  • ReadLine: Input a string and use it in the continuation

Step 2: Smart Constructors #

Create convenient functions to build Free Monad programs:

Step 3: Write Programs Using the DSL #

Now we can write programs that look monadic but don't commit to any interpretation:

Step 4: Multiple Interpreters #

Here's where Free Monads shine - we can interpret the same program in different ways:

  • Real Console Interpreter

  • Test Interpreter (Pure)

  • Mock Trace - Unit Tests

    This trace collector records the operations and inputs but discards the program's result. Unlike the IO and test interpreters, it is not a monad homomorphism Free ConsoleF → m.

Step 5: Running Your Programs #

  1. Same Logic, Different Contexts: greetingProgram works in production (IO), testing (pure), and mocking
  2. Testability: You can unit test your business logic without actual I/O
  3. Flexibility: Add new interpreters (logging, debugging, optimization) without changing programs
  4. Composability: Programs compose naturally using monadic operations

Core power of Free Monads: separation of description from interpretation. Your programs describe what to do; interpreters decide how to do it.

Free Monad Visualization #

Visually, Free Monads represent a separation between structure and interpretation. Every Free Monad computation can be seen as building a syntax tree that gets interpreted later.

FREE MONAD STRUCTURE           INTERPRETER EXECUTION
────────────────────           ─────────────────────

Program Description            Program Execution
┌─────────────────┐            ┌─────────────────┐
│                 │            │                 │
│   Free f a      │ ---------> |     m a         │
│  (Syntax Tree)  │ interpret  │  (Real Effect)  │
│                 │            │                 │
└─────────────────┘            └─────────────────┘
  Pure structure                Effectful computation

Data Structure Tree            Evaluation Strategy
┌─────────────────┐            ┌─────────────────┐
│      Pure a     │ -- fold -->│     return a    │
└─────────────────┘            └─────────────────┘

┌─────────────────┐            ┌─────────────────┐
│  Free(f(...))   │ -- fold -->│   interpret f   │
└─────────────────┘            └─────────────────┘

1. Free Monad Construction vs Interpretation

CONSTRUCTION PHASE                INTERPRETATION PHASE
──────────────────                ────────────────────

Build Abstract Syntax            Execute with Strategy

    Operations                        Interpreters
  ┌─────────────┐                 ┌─────────────────┐
  │ WriteLine   │                 │    putStrLn     │
  │ ReadLine    │    --------->   │    getLine      │
  └─────────────┘                 └─────────────────┘
   Pure DSL                        Concrete Effects

  Free Monad Tree                  Execution Tree
┌─────────────────┐               ┌─────────────────┐
│ Free WriteLine  │               │   putStrLn      │
│        │        │               │        │        │
│ Free ReadLine   │ ------------> │    getLine      │
│        │        │               │        │        │
│      Pure b     │               │    return b     │
└─────────────────┘               └─────────────────┘
   Description                     Interpreted


2. Multiple Interpretation Strategies

SINGLE FREE MONAD                 MULTIPLE INTERPRETERS
─────────────────                 ─────────────────────

  One Program                     Many Strategies
┌─────────────────┐
│                 │               ┌─── Real IO ────────┐
│  ConsoleProgram │               │   putStrLn         │
│      Free       │ ------------> │   getLine          │
│   ConsoleF a    │               └────────────────────┘
│                 │
└─────────────────┘               ┌─── Test Mock ──────┐
        │                         │  ["output1"]       │
        │                         │  ["input1","in2"]  │
        │                         └────────────────────┘
        │
        │                         ┌─── Logging ────────┐
        │                         │  log operations    │
        └─────────────────────────│  trace execution   │
                                  └────────────────────┘

Same Structure, Different Meanings

3. Free Monad Bind Operation

MONADIC BIND IN FREE MONADS
───────────────────────────

Free f a >>= k  where k : a → Free f b

Case 1: Pure Value
┌─────────────┐            ┌─────────────┐
│   Pure a    │ >>= k ---> │     k a     │
└─────────────┘            └─────────────┘
Direct application

Case 2: Suspended Computation
┌─────────────────────┐            ┌──────────────────────┐
│ Free (f (Free f a)) │ >>= k ---> │ Free (fmap(>>= k)f)  │
└─────────────────────┘            └──────────────────────┘

Recursive bind propagation

Visual Flow:
     Free f a
        │
    >>= │ k : a → Free f b
        │
        ▼
  ┌─────────────┐
  │ Is it Pure? │----------
  └─────┬───────┘          │
        │                  │
    ┌───▼───┐         ┌────▼────┐
   ┌┴──Yes──┴┐       ┌┴───No────┴┐
   │ Pure a  │       │ Free f... │
   └─────────┘       └───────────┘
        │                  │
        ▼                  ▼
   ┌─────────┐    ┌─────────────────────┐
   │  k a    │    │ Free (fmap(>>= k)f) |
   └─────────┘    └─────────────────────┘

Conclusion #

Free Monads represent a solution to a fundamental problem: how to build complex, effectful programs while maintaining the ability to reason about them, test them, and interpret them in multiple ways.

Where Comonads imprisoned computations within their spatial context, Free Monads offer the opposite extreme: complete liberation from interpretation until the last possible moment. This philosophical shift from "immediate execution" to "deferred description" unlocks unprecedented flexibility in program design.

A Free Monad is just data that looks like a computation

This simple insight has profound implications:

  1. Separation of Concerns: What your program does vs. how it does it
  2. Multiple Interpretations: Test, production, optimization, debugging - all from one description
  3. Compositional Safety: Build complex operations from simple, reliable building blocks
  4. Pure Reasoning: Think about effects without being entangled by them

Free Monads aren't without cost:

  • Performance: Building syntax trees has overhead compared to direct execution. For performance-critical code, this indirection may be prohibitive.

  • Complexity: Free Monads introduce concepts that can overwhelm developers unfamiliar with categorical thinking.

  • Memory: Large Free Monad computations create substantial data structures before interpretation.

  • Learning Curve: The abstraction requires understanding functors, monads, and categorical relationships.

Free Monads shine when you need:

  • Multiple execution strategies for the same logic
  • Comprehensive testing of effectful code
  • Domain-specific languages with clean separation of syntax and semantics
  • Modularity where effect interpretation can be swapped out

Source code #

Reference implementation (opens in a new tab)

References

  1. Free structure (opens in a new tab)
  2. Free Monad (opens in a new tab)