Monads

From bibbleWiki
Jump to navigation Jump to search

Introduction

Heard about this with a friend and never really revisited it. With my attention at home on Rust, thought I might write something down

The Maybe Monad

Well possibly the Option monad for me. To make one of these you need define something to wrap your thing with and a function which could fail that returns the wrapper type

return :: a => Maybe a
>>= :: Maybe a

This was explained to me a little better with this picture.

What's the Point

  • Same idea works for other effects, e.g. reading from environments, input/output
  • Supports pure programming with effects
  • Use of effects explicit in types
  • Functions that work for any effect

Second Time Through

Really want to get this into my head. It is another Mathy thing that just requires me to have a light bulb moment.

Step 1 - Starting Code

So the YouTube started off with this which I might need to change

function square(x: number): number {
    return x * x;
}

function addOne(x: number): number {
    return x + 1;
}

// Which allows us to chain the two together. e.g.
addOne(square(2)) => 5

// I think this is the goal. To do something extra which uses the input data
// This example confused me because it is not valid typescript
addOne(square(2)) => {
    result: 5,
    logs: [
        "square(2) => 4",
        "addOne(4) => 5"
    ]
}

I think what they were suggest is they want the two functions to return an object with logs e.g.

const result = addOneWithLogs(squareWithLogs(2))
// Where result is
{
    result: 5,
    logs: [
        "square(2) => 4",
        "addOne(4) => 5"
    ]
}

Step 2 - Implement new Funcs

First we define an interface to hold the result we are after. i.e. the result and logs

interface NumberWithLogs {
    result: number;
    logs: string[];
}

Now we make our new addOneWithLogs and squareWithLogs to get our result

function squareWithLogs(x: number): NumberWithLogs {
    const result = x * x;
    return {
        result,
        logs: [`Squared ${x} to get ${result}`]
    };
}

function addOneWithLogs(x: NumberWithLogs): NumberWithLogs {
    const result = x.result + 1
    return {
        result, 
        logs: x.logs.concat([
            `Added one to ${x.result} to get ${x.result + 1}`
        ])
    };
}

const result2 = addOneWithLogs(squareWithLogs(2));

// Gives
{
  result: 5,
  logs: [ 'Squared 2 to get 4', 'Added one to 4 to get 5' ]
}

Step 3 - Identifying real Requirements

They want to be able to do this

squareWithLogs(squareWithLogs(2))
addOneWithLogs(5)

This is not possible with the current implementation as addOneWithLogs and squareWithLogs functions both take a ``NumberWithLogs``` as an argument not a ```number```

Step 4 - Wrapping the arguments

The next thing we do is to create a function which does take a number and converts it to a NumberWithLogs so we can call squareWithLogs(squareWithLogs(2)) and addOneWithLogs(5)

function wrapWithLogs(x: number): NumberWithLogs {
    return {
        result: x,
        logs: []
    };
}

We can now looking at squareWithLogs we can change the argument from number to squareWithLogs

function squareWithLogs(x: NumberWithLogs): NumberWithLogs {
    const result = x.result * x.result;
    return {
        result,
        logs: x.logs.concat([
            `Squared ${x.result} to get ${result}`
        ])
    };
}

Now we can call squareWithLogs

const result = squareWithLogs(squareWithLogs(wrapWithLogs(2)))

// Gives
{ result: 16, logs: [ 'Squared 2 to get 4', 'Squared 4 to get 16' ] }

Step 5 - Reviewing Code

We need to look at our code for duplication.

Original Code No Change

Here is the original code

function addOneWithLogs(x: NumberWithLogs): NumberWithLogs {
    const result = x.result + 1
    return {
        result, 
        logs: x.logs.concat([
            `Added one to ${x.result} to get ${x.result + 1}`
        ])
    };
}

function squareWithLogs(x: NumberWithLogs): NumberWithLogs {
    const result = x.result * x.result;
    return {
        result,
        logs: x.logs.concat([
            `Squared ${x.result} to get ${result}`
        ])
    };
}

Rework Version 1

Let's change the return on both to calculate the logs outside of the return then the return function will look the same.

function addOneWithLogs(x: NumberWithLogs): NumberWithLogs {
    const result = x.result + 1
    const thisLogs = [
        `Added one to ${x.result} to get ${result}`
    ];
    return {
        result, 
        logs: x.logs.concat(thisLogs)
    };
}

function squareWithLogs(x: NumberWithLogs): NumberWithLogs {
    const result = x.result * x.result;
    const thisLogs = [
        `Squared ${x.result} to get ${result}`
    ];

    return {
        result,
        logs: x.logs.concat(thisLogs)
    };
}

Rework Version 2

Now lets wrap the top half of both in a NumberWithLogs. Notice the return is the same for both

function addOneWithLogs(x: NumberWithLogs): NumberWithLogs {
    const numberWithLogs: NumberWithLogs = {
        result: x.result + 1,
        logs:  [
            `Added one to ${x.result} to get ${x.result + 1}`
        ]
    }
    return {
        result: numberWithLogs.result,
        logs: x.logs.concat(numberWithLogs.logs)
    };
}

function squareWithLogs(x: NumberWithLogs): NumberWithLogs {
    const numberWithLogs: NumberWithLogs = {
        result: x.result * x.result,
        logs: [
            `Squared ${x.result} to get ${x.result * x.result}`
        ]
    }

    return {
        result: numberWithLogs.result,
        logs: x.logs.concat(numberWithLogs.logs)
    };
}

Step 5 - The NumberWithLogs Transform function

So looking at both functions we can see the top half is a function which takes x as an argument and returns a NumberWithLogs. e.g.

const foo = (x: NumberWithLogs):NumberWithLogs  => {
 ....
}

This is called a transform and in our case it might be addOne or square. Either way we start with a NumberWithLogs and return a NumberWithLogs. So lets express these as squareTransform and addOneTransform

const squareTransform = (x: NumberWithLogs): NumberWithLogs => {
    return {
        result: x.result * x.result,
        logs: [
            `Squared ${x.result} to get ${x.result * x.result}`
        ]
    }
}

const addOneTransform = (x: NumberWithLogs): NumberWithLogs => {
    return {
        result: x.result + 1,
        logs:  [
            `Added one to ${x.result} to get ${x.result + 1}`
        ]
    }
}

Step 6 - The n Transform function

We can see above we do not reference the x.logs in the transform so we can simplify these transforms to just take a number and they will work the same way.

const squareTransform = (x: number): NumberWithLogs => {
    const result = x * x;
    return {
        result,
        logs: [`Squared ${x} to get ${result}`]
    }
}

const addOneTransform = (x: number): NumberWithLogs => {
    const result = x + 1;
    return {
        result,
        logs:  [`Added one to ${x} to get ${result}`]
    }
}

Step 6 - The RunWithLogs function

Now we can run either of these functions with this

const runWithLogs = (
    input: NumberWithLogs, 
    transform: (x: number) => NumberWithLogs): NumberWithLogs => {

    const numberWithLogs = transform(input.result);
    return {
        result: numberWithLogs.result,
        logs: input.logs.concat(numberWithLogs.logs)
    }   
}

🧭 Conclusion: My First Monad

In exploring monads, I built a simple wrapper called NumberWithLogs that tracks both a result and a log of operations. This helped me understand how monads let us:

  • Wrap values with extra context (like logs)
  • Chain operations while keeping that context
  • Keep the code readable and composable

🧱 Monad Building Blocks (Simplified)

Concept Simple Explanation My Example Also Known As
Wrapper Type A container that holds a value and extra info (like logs) NumberWithLogs
Wrap Function Takes a plain value and puts it into the container wrapWithLogs return, pure, unit
Run Function Applies a transformation to the value, keeping the container structure runWithLogs bind, flatMap, >>=

🧪 Transform Functions I Used

Each function takes a number and returns a NumberWithLogs — a result plus a log message.

const squareTransform = (x: number): NumberWithLogs => {
    const result = x * x;
    return {
        result,
        logs: [`Squared ${x} to get ${result}`]
    };
};

const addOneTransform = (x: number): NumberWithLogs => {
    const result = x + 1;
    return {
        result,
        logs: [`Added one to ${x} to get ${result}`]
    };
};

🧰 Monad Functions

function wrapWithLogs(x: number): NumberWithLogs {
    return {
        result: x,
        logs: []
    };
}

const runWithLogs = (
    input: NumberWithLogs,
    transform: (x: number) => NumberWithLogs
): NumberWithLogs => {
    const numberWithLogs = transform(input.result);
    return {
        result: numberWithLogs.result,
        logs: input.logs.concat(numberWithLogs.logs)
    };
};

🧠 What I Learned

  • Monads help manage extra context (like logs) without cluttering logic
  • They make chaining operations predictable and clean
  • Even simple wrappers can teach deep ideas about functional design

Finally

The YouTube left us with this slide. I guess the most obvious ones are Option, Result, Writer, Future/Promise