Monads

## What are they?

A monad is a data type (e.g **int**) that encapsulates some control flow (e.g **try/catch**).

## Why are they important?

The key to understanding the benefit of monads is realizing **expressions themselves don't cause exceptions, the evaluation causes exceptions**.

If you can resolve your expression to a type that can be evaluated later, you may be able to handle exceptions as part of a type definition, which could provide a handle for static analysis tools or compile-time guarantees to hook into.

So monads can help us implement a safer version of a expression without suffering many of the negative side effects that come with loading up our program with a bunch of different try/except statements.

For example:

```
        return a / b
    except ZeroDivisionError:
        pass # WHAT TO DO HERE??
    # General catch-all for unhandled
    # edge cases
    except Exception as err:
        raise err

```

The above would end up leading to many different **except** clauses, which would expand our **try/catch** block with respect to the number of exception types you want to catch.

Also if you apply exception handling as part of any mainline program execution, you lose the ability to accurately trace and exceptions from the handling logic and before.

## Maybe, Just, Nothing

Here is the initial definition of the **Maybe**, **Just**, and **Nothing** monads:

```
class _Maybe(object):

```

This class definition exists to add any monadic attributes or operators.

Since we are only talking about **Maybe**, **Nothing**, and **Just**, and since **Nothing** and **Just** inherit from **Maybe**, we'll wrap any basic and monadic attributes as part of this class definition.

```
def Maybe(cls):
    raise NotImplentedError

```

Parametrization of a typedef to include monadic attributes. This method enables dynamic classdef generation by inheriting a base class passed in as an input argument and overriding class attribute '**\_\_base\_\_'**

```
class _Just((_Maybe):
# A successful evaluation of a Maybe monad.

def Just(cls):
    # Functional instantiation of **Just**
    return type('Just(%s) % cls.__name__, (_Just, cls), {})

def _Nothing(_Maybe):
    # A failed evaluation of a Maybe monad.

# >>>
Nothing = _Nothing()

```

This results in the following:

```
>>> Just(int)(1)
1
>>> type(Just(int)(1))
<class

```

What this bit of logic does is apply multiple inheritance to render effects onto a parametrized base type. This is further made functional by using the first-class function **type()** and the extended constructor method that updates the underlying object attribute **\_\_base\_\_**.

This satisfies one property of the Maybe monad, in that they can turn pure effects (e.g a mathematical expression) into impure effects (e.g essentially replacing a **try/catch** statement with a type).

So a first-order approximation, such as let's say a division opperation, which looks like this:

```
def div(a : int, b : int) -> float:
    return a / b

```

Can be turned into this:

```
def safediv(a : Maybe(int), b : Maybe(int)) -> Maybe(float):
    if (
        a is Nothing or
        b is Nothing or
        b == 0
    ):
        return Nothing
    return Just(float)(a / b)

```

The type parametrization aspect of monads (at least in our above context), creates a bound on how a portion of control flow should fail: it simply returns **Nothing**. Because it's a type that represents an error, and because calling a monadic method with **Nothing** generates **Nothing** (enforcing idempotency), the runtime doesn't need to worry about having to raise an exception to avoid propagating an error that cannot be handled later on.

As long as the types match, it is safe to run. Errors become safe to pipeline, and pipelining allows systems of arbitrary complexity to be developed.

This is powerful because it linearizes your error model. No matter how complex your pipeline may get, you should only ever expect to get more **Nothing** types with a longer pipeline, and not totally new types of errors.

You may think that this makes debugging harder because all failures are of type **Nothing**. This isn't necessarily true.

Not only can you log the error message/input arguments/metadata to reproduce the error as part of the monad definition, you can also create new monad definitions whenever you want.

*The **Maybe** type is sometimes used to represent a value which is either correct or an error; by convention, the **Nothing** constructor is used to hold an error value and the **Just** constructor is used to hold a correct value.*
Let's try implementing binding/sequencing attributes for out monad classes:

```
        self.data = data
        raise NotImplementedError

    def bind(self, function):
        raise NotImplementedError

            error_message = 'Can only pipeline monads.'
            raise TypeError(error_message)

        if not callable(function):
            function = lambda _ : function

        return self.bind(function)

# ...

        self.data = data

    def bind(self, function):
        return function(self)

# ...

class \_Nothing(_Maybe):
    """A failed evaluation of a Maybe monad.
 """
        return "Nothing"

    def bind(self, _):
        return self

```

## References

* https://bytes.yingw787.com/posts/2019/12/06/monads/