The Tecnoprom Core

Advanced and not-so-advanced technology

See my new website: DSSTI

Monads in Python

On my quest to improve my code and learn more of the craft, I started learning Haskell. I knew a bit of OCaML, but I didn't have the experience to actually do proper functional programming. Haskell forces you to do it, because it's purely functional, and doesn't support standard OO abstractions, which at the end of the day were the way I "cheated" to make a 3D engine in OCaML without applying too much functional programming.

But there was something I remembered from OCaML, which I missed in all the other languages: a proper algebraic type system. OCaML's option type is the reason why NullPointerException will never take off in functional languages. In Haskell, that is called the Maybe data type. But Haskell wasn't willing to stop there. Once you have a computation that may succeed or not, returning Just x, or Nothing, you don't stop there. You continue by chaining computations. If all of them succeed, you get a Just with the result, and if any of them fails, you get Nothing.

But chaining stuff the "standard" way is ugly. You have to check after each computation if you got Just or Nothing, and act accordingly. If only we could factor out the check...

Monads to the rescue

The monad is a general way of chaining computations. Depending on the situation, the chaining will work in different ways. For example, the Maybe monad chains computations in a way that if all of them are successful, you get Just with a result, but if any of them fails you get Nothing. The monad factors out the check after each computation, and you only have to check the final value.

Maybe is the simplest and most popular monad. That's why is the prime example for demonstrating monads. Its usefulness extends far away, not only in Haskell. Here we reimplement the Maybe monad in Python, because in Python it is also useful to have several computations that can be chained.

But first we need to define failed computations.

Failed computations

We will organise our computations in functions. We define a failed computation in Python as a computation that fails in a predictable way: raising an exception. So either the computation is successful, and the function returns a useful value, or the computation fails, and it raises an exception.

Maybe monad

Now we have enough to define the maybe monad. I'll use a class to put everything in a contained namespace:

class Maybe(object):
    """
    Maybe monad in Python
    Any error in the wrapped function will make the function return None
    """
    @classmethod
    def lift(cls, wrapped_fn):
        """
        Decorator
        Converts a function to use the Maybe monad
        @param wrapped_fn: a function that accepts a single parameter
        """
        def fn((status, arg)):
            if status == "failed":  # short circuit
                return ("failed", None)
            else:
                try:
                    result = wrapped_fn(arg)
                    return "success", result
                except Exception:
                    return "failed", None
        return fn

    @classmethod
    def ret(arg):
        """
        return function for the Maybe monad
        @param arg: the argument to wrap
        """
        return ('success', arg)

    @classmethod
    def extract((status, arg)):
        """
        Extracts the value boxed in a Maybe monad
        @param: the value
        @return: the unboxed value,
                 or raises Exception("Unsuccessful computation")
        """
        if status == 'success':
            return arg
        else:
            raise Exception("Unsuccessful computation")


op1 = Maybe.lift(fun1)
op2 = Maybe.lift(fun1)
op3 = Maybe.lift(fun1)
result = Maybe.extract(op1(op2(op3(Maybe.ret(value)))))

But why?

Sure, we have made the Maybe monad in python. But it is worth it? Well, not for this case. The Maybe monad is way easily integrated in Python as a try-except block:

try:
    val1 = fun1(value)
    val2 = fun2(val1)
    val3 = fun3(val2)
    return val3
except Exception:
    print "Something went wrong"
So here is the conclusion:
  • Python is awesome
  • Haskell is awesome
  • Haskell in Python may not be awesome
  • Use the right tool for the right job