Failure monad


Martin McBride, 2020-06-25
Tags monad failure monad design pattern
Categories functional programming

This article is part of a series on 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.

Unfortunately, the int, neg and 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 and str too. 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.

Syntax improvement

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)

Summary

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.

See also

If you found this article useful you might be interested in my ebook Functional Programming in Python.


Tag cloud

2d arrays abstract data type alignment and array arrays bezier curve built-in function close closure colour comparison operator comprehension context conversion data types design pattern device space dictionary duck typing efficiency encryption enumerate filter font font style for loop function function plot functools generator gif gradient html image processing imagesurface immutable object index input installing iter iterator itertools lambda function len linspace list list comprehension logical operator lru_cache mandelbrot map monad mutability named parameter numeric python numpy object open operator optional parameter or path positional parameter print pure function radial gradient range recursion reduce rotation scaling sequence slice slicing sound spirograph str stream string subpath symmetric encryption template text text metrics transform translation transparency tuple unpacking user space vectorisation webserver website while loop zip

Copyright (c) Axlesoft Ltd 2020