29
Monads In Python
Monads are notoriously confusing, I think part of this has to do with the fact that most monad tutorials are in Haskell which most programmers don't know, as well as being explained in a mathematical context with scary names like Functors, Applicatives, and Monoids. The simplest definition of a Monad is a context attached to a value that allows that value to be sequenced. This is powerful in that it allows sequential and procedural programming in functional languages that don't have it by default. Monadic-like things exist in some popular languages already like Promises in Javascript and the with statement in Python. Both of which control the flow of the program.
To start off lets start with a basic Monad used in Haskell and we can also implement in Python. The Maybe monad is used to attach a failure context to a value, like .error() in Javascript or maybe similar to a try-catch. This failiure context is passed down to the end of the execution and can be obtained at the end so we don't call a method on the value. This means no null-pointer exceptions, oh the joy.
class Maybe:
def __init__(self, value):
self.value = value
def __repr__(self):
return f"Maybe({self.value})"
def unwrap(self):
return self.value
def bind(self, func):
if self.value == None:
return Maybe(None)
return Maybe(func(self.value))
@staticmethod
def wrap(value):
return Maybe(value)
You can see how we have a method here called bind (The >>= operator in Haskell) which is the most important one. Bind takes in a function which takes in a non-monadic value and makes it monadic, and applies the monadic value to that function. Sort of like map does in python, but not necessarily only to a list. In fact bind can work on any value, that is why it's so useful. It can be the response from an HTTP request or a file handle, etc. All of which can fail at some point and we need to capture that failure. In the bind method, if our value is None we just return None, this propagates the error down the chain of monadic computations. If it's not none then we just pass the value to the function and create a new Maybe monad from that.
Let's see what happens when we run bind several times.
maybe = Maybe(4).bind(lambda x: 2*x).bind(lambda y: y+1)
print(maybe)
print(maybe.unwrap())
We get the output
Maybe(9)
9
The function was applied several times and seemed to succeed. Now let's introduce a function that could fail. If it does fail we return None and otherwise return the value.
def this_could_fail(x):
return None if x < 10 else x
maybe = Maybe(4).bind(this_could_fail).bind(lambda x: 2*x).bind(lambda y: y+1)
print(maybe)
print(maybe.unwrap())
Notice how we called bind on this_could_fail and since our value inside our Maybe is less than 10 it returned None. This None was propagated through the bind chain letting us know at the end that somewhere the computation failed. We could have done a check at the end to see if the computation failed like.
if maybe is None:
print("Computation failed")
else:
print("Computation didn't fail")
Maybe is a monad with a failure context, if this looks familiar it's similar to Promises in Javascript. We could even replace .then with .bind, and use it similarly.
def do_something_with_response(res):
if res is not None:
print(res.text)
Maybe(requests.get("http://somesite.com/")).bind(do_something_with_response)
Maybe takes in an http request and then processes the request in bind similar to how you would use then. You could stack several functions on it and keep using the response in each one.
The second type of monad I'd like to cover is the List monad. The list monad is similar to map or list comprehensions, except it can do more than just mapping a function to a list.
class List:
def __init__(self, value):
self.value = list(value)
def __repr__(self):
return f"List({self.value})"
def unwrap(self):
return self.value
def concat(self, list_lists):
return [item for sublist in list_lists for item in sublist]
def bind(self, func):
ls = self.concat([func(x) for x in self.value])
return List(ls)
@staticmethod
def wrap(value):
return List(value)
We have a new method here, concat which takes a list of lists and appends them together to make a flat list. We also have the method bind which maps func to the values in the monad and then calls concat on that. This creates a sort of "list generator", a powerful way of generating lists based on a function.
We can call it like this
ls = List([1,2,3,4,5]).bind(lambda x: [x, -x])
print(ls)
print(ls.unwrap())
This passes in the values of the list monad into our function which then creates a list like [[1, -1], [2, -2], [3, -3], [4, -4], [5, -5]]. This list is then flattened by concat to produce our output.
List([1, -1, 2, -2, 3, -3, 4, -4, 5, -5])
[1, -1, 2, -2, 3, -3, 4, -4, 5, -5]
You can think of the function in our bind as a generator producing new lists based upon the value of monad and then joining those lists together. We can also use it with strings, and when we append our string in the bind function we will see how powerful this can be.
ls = List(["Dog", "Kitty"]).bind(lambda x: ["Smelly " + x, "Good " + x])
print(ls.unwrap())
The output of this is a list of all the combinations from the two lists in the monadic chain. We get this by concatenating the input string in the lambda in bind. This produces several strings which take all possible values.
['Smelly Dog', 'Good Dog', 'Smelly Kitty', 'Good Kitty']
We can do something similar with list comprehensions like this.
[y + " " + x for x in ["Dog", "Kitty"] for y in ["Smelly", "Good"]]
List comprehensions in Haskell themselves are monads. In python we can implement our own version of them using the List monad. We also have a type of error checking with the List monad as well.
ls = List([]).bind(lambda x: [x, -x])
print(ls.unwrap())
When we run this we get the empty list. This is like the Maybe monad with its behavior when None is returned. In this case the empty list will be propagated down the function chain. In this way errors can be propagated within the monadic chain.
Now you might be asking what use do we have for monads out side of functional programming?
Several actually. Like I said before Monads already kind of exist in Javascript with promises. We can do something like
doComputation().then(doThisIfSuceeded).error(doThisIfFailed)
This sort of functional chain is already a monad. We can use it like the Maybe monad to determine when a failure occurs instead of using complicated exception handling routines or null pointer checks.
Another usage of monads might be to log intermediate values, for example in debugging when we need to know what values are passed into a function and how they are modified. We can already kind of do this with decorators in python, but monads can also be helpful here. For example let's say I have a Logging monad which prints to the console the current value when bind is called.
Log(10).bind(lambda x: x**2).bind(lambda y: x+1)
Then the monad would print out 100 and then 101, as the intermediate values. This can be done on complicated monadic chains, allowing us to debug statements with difficult to understand control flow.
I encourage you to look up monads in Haskell to see the different variety of monads that exist. If you still don't understand them after this article I don't know if I can help you. Just explore with the code I have here. Monads aren't that difficult to understand when you understand them in the context of things we already know like function chaining, promises, and list comprehensions. They are simply values with an added context that lets us do more complicated computations with the value.
29