TrashPanda Wiki
Advertisement

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:

def div(a, b):
    try:
        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.

# Ensure that `Nothing` is unique; uniqueness implies same `id()` result, which
# implies a global object (singleton), much like `None = type(None)()` is a
# singleton.


# >>> id(type(None)())
# 4562745448 # Some object ID, this may vary.
# >>> id(None)
# 4562745448 # Same object ID as before.
# >>>
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:

class _Maybe(object):
    def __init__(self, data=None):
        self.data = data
        raise NotImplementedError

    def bind(self, function):
        raise NotImplementedError

    def __rshift__(self, function):
        if not isinstance(function, _Maybe):
            error_message = 'Can only pipeline monads.'
            raise TypeError(error_message)

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

        return self.bind(function)

# ...

class _Just(_Maybe):
    def __init__(self, data=None):
        self.data = data

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

# ...

class _Nothing(_Maybe):
    """A failed evaluation of a Maybe monad.
    """
    def __init__(self, _=None):

    def __str__(self):
        return "Nothing"

    def bind(self, _):
        return self

References[]

Advertisement