Monads are the most feared concept of FP, so I reserve a complete chapter for understanding this concept.
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.
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]:
In [4]:
cp1.get_address()
Out[4]:
In [5]:
cp1.get_address().get("street")
Out[5]:
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]:
In [8]:
cp2.get_address().get("street")
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]:
In [27]:
get_street_from_company(cp3)
In [ ]: