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