Martin McBride, 2020-06-25
Tags monad failure monad design pattern
Categories functional programming
Monads have a reputation for being difficult to understand, but in this article we will look at a simple Python implementation of a simple monad. This will hopefully give some insight into what monads are all about.
What is a monad
In functional programming, we use functions as our primary building block, and function composition as one of the basic ways to structure our code. By function composition we mean functions that call other functions, like this:
y = str(neg(int(x)))
This composed function takes a string, converts it to an integer, negates it, and converts it back to a string. So the string "1" is converted to "-1".
But there is a problem. What if our initial string is something like "XYZ"? Then
int will throw a ValueError exception because it can't convert that string to an integer value. We will end up in an exception handler somewhere else in our code, or worse still our program might terminate with an error message. This is exactly the sort unpredictability the functional programming is supposed to solve.
Now imagine if the
int function was able to return not only the integer value, but also a flag to say whether the conversion had succeeded or failed. This failed flag could be thought of as a context around the actual data - if the failed flag is false, the data is valid, if the failed flag is true we should ignore the data as it isn't valid.
Even better, suppose that the
neg function could also take account of the context. When it receives a value from the
int function, it checks the failed flag. If it is false,
neg knows that the data is valid, so it negates the value and returns it with the failed flag set to false. If the failed flag is true,
neg doesn't process the value at all, it just returns a context with the data set to
None and the failed flag true.
And ideally, of course, the
str function would do exactly the same thing. That means that the composed function above would always return a value. It would never throw an exception. But the value would have a context to say whether it was valid or not.
str functions don't behave like this. That is where monads come in useful.
A monad is a design pattern that allows us to add a context to data values, and also allows us to easily compose existing functions so that they execute in a context aware manner.
The Failure monad
We will now implement the monad described above, which we will call the Failure monad. Notice that monads are a design pattern. Design patterns describe a general approach to solving a particular type of problem, but they don't dictate the exact implementation. The version here is written for simplicity and clarity above all else.
Here is a simple implementation of Failure:
class Failure(): def __init__(self, value, failed=False): self.value = value self.failed = failed def get(self): return self.value def is_failed(self): return self.failed def __str__(self): return ' '.join([str(self.value), str(self.failed)]) def bind(self, f): if self.failed: return self try: x = f(self.get()) return Failure(x) except: return Failure(None, True)
This class holds a value, and a failed flag, that are set when the object is created. It also overrides the
__str__ function. This function implements the
str() built-in function and is also called when you print the object. It displays both the value and the failure state of the object.
The thing that turns this into a monad is the
bind function. It is used like this:
x = '1' y = Failure(x).bind(int) print(y)
In this code, we create a Failure monad with the value "1". We then bind the int function to the monad. Here is what bind does:
- It extracts the value from the monad.
- It applies the function to the value.
- It then returns the value wrapped in a new monad.
The print statement prints the value (1) and the failed flag (False, because the operation succeeded).
Since bind returns a Failure monad, we can apply
neg is actually a function from the operators module:
from operator import neg x = '1' y = Failure(x).bind(int).bind(neg).bind(str) print(y)
This does the same as
str(neg(int(x))), but with monads. But here is the clever bit:
x = 'XYZ' y = Failure(x).bind(int).bind(neg).bind(str) print(y)
In this case,
int throws an exception because "XYZ" can't be converted to an integer. The bind method catches this exception, and returns
Failure(None, True) - a Failure Monad that indicates a failed state.
When we bind
neg to this value, the bind method detects that the failed flag is set. It doesn't call
neg at all, it juts returns itself (ie the failed monad). The same thing happens when we bind
str to the result. So:
- If all the functions operate without failure, the final result is a Failure monad containing the calculated value.
- If any function fails, the remaining functions don't execute and the result is a Failure monad with the failed flag set.
We can add the following method to our monad, to override the or operator (symbol |) to invoke bind:
def __or__(self, f): return self.bind(f)
With this new definition we can use a simpler syntax for chaining functions:
x = '1' y = Failure(x) | int | neg | str print(y)
This was a simple introduction to the idea of monads via a very basic implementation of one of the simplest monads. The concept of a context is very general, and can go far beyond a simple flag. But I hope this introduction might give you a small insight into how monads work.