Monads

Monads are the most feared concept of FP, so I reserve a complete chapter for understanding this concept.

What is a monad?

Right now, my understanding is that monads are a very flexible concept that basically allows to attach context to an otherwise stateless system. This means, that through a monad, the application of a otherwise pure function can be made dependent on context, so that a function will be executed differently in different contexts.

An easy example: The maybe monad

We will start with an easy example: Let's assume we have the task of looking up a street name from a company record. If we'd do it the normal, non-functional way, we'd have to write functions that look up these records and check if the results are not NULL:

This example is heavily inspired by https://unpythonic.com/01_06_monads/

The following is a simple company class, where the address attribute is a simple dict containing the detailed address information.


In [1]:
class Company():
    def __init__(self, name, address=None):
        self.address = address
        self.name = name
        
    def get_name(self):
        return self.name
    
    def get_address(self):
        return self.address

I now instatiate an instance of this class with a correctly set street attribute in the address dict. Then, everything works well when we want the query the street address from this company:


In [2]:
cp1 = Company(name="Meier GmbH", address={"street":"Herforstweg 4"})

In [3]:
cp1.get_name()


Out[3]:
'Meier GmbH'

In [4]:
cp1.get_address()


Out[4]:
{'street': 'Herforstweg 4'}

In [5]:
cp1.get_address().get("street")


Out[5]:
'Herforstweg 4'

However, when we want to get the street name when the company doesn't have a street attribute, this lookup will fail and throw an error:


In [6]:
cp2 = Company("Schultze AG")

In [7]:
cp2.get_name()


Out[7]:
'Schultze AG'

In [8]:
cp2.get_address().get("street")


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-8967935b7eee> in <module>()
----> 1 cp2.get_address().get("street")

AttributeError: 'NoneType' object has no attribute 'get'

What we would normally do to allieviate this issue is to write a function that deals with null values:


In [9]:
def get_street(company):
    address = company.get_address()
    if address:
        if address.has_key("street"):
            return address.get("street")
        return None
    return None

In [10]:
get_street(cp2)

In [11]:
cp3 = Company(name="Wifi GbR", address={"zipcode": 11476} )

In [12]:
get_street(cp3)

We now see that we are able to complete the request without an error, returning None, if there is no address given or if there is no dict entry for "street" in the address.

But wouldn't it be nice to have this handled once and for all?

Enter the "Maybe" monad!


In [13]:
class Maybe():
    def __init__(self, value):
        self.value = value

    def bind(self, fn):
        if self.value is None:
            return self
        return fn(self.value)

    def get_value(self):
        return self.value

Now, we can rewrite the get_street as get_street_from_company, using two helper function


In [25]:
def get_address(company):   
    return Maybe(company.get_address())

def get_street(address):
    return Maybe(address.get('street'))

def get_street_from_company(company):
    return (Maybe(company)
            .bind(get_address)
            .bind(get_street)
            .get_value())

In [26]:
get_street_from_company(cp1)


Out[26]:
'Herforstweg 4'

In [27]:
get_street_from_company(cp3)

In [ ]: