Hacker News new | ask | show | jobs
by tel 4916 days ago
They're not smart to do in python. Monads are an expressive, simple pattern of coding that can be type-checked. In dynamic languages it'll just look like a lot of unnecessary line noise. In HM type systems it's a great way to help you pass contexts around in a way lets you focus on your values.

But if you just want to speak Python, let me try to translate.

---

As a motivation for monads in Python, we're going to try to make "total" Python. To do so, we have to eschew execptions. This means we'll write stuff like

    def mypop(l):
      if len(l) > 0:
        return l.pop()
      else:
        return None
which, if we pass it a list of numbers, is guaranteed to return either a number or None---I've sidestepped the empty list exception. Now let's say I want to compose this function. For instance, my list of numbers is a list of bids and I have a function where I can try buying something with my bid.

    def buy(tendered):
      effective = tendered*1.2 # there's a discount!
      if market_open():
        if effective > 60:
          return True
        else:
          return False
      else:
        return None
So I believe that buy takes numbers and returns Booleans, so I might think that

    def trybuy(l): return buy(mypop(l))
takes lists of numbers and returns a boolean as to whether I've bought it. Of course, I have to be smart enough not to send in an empty list otherwise the discount in 'buy' will cause an error. I also have to be sure that if I try buying on days the market is closed to handle the 'None'. In Haskell, we'd write these constraints into the types and values using a Maybe type.

Here's a Pythonic Maybe type (kind of not pythonic though with the magic sentinel value, but we don't want to be able to sneak 'None's in).

    class Maybe(object):

      __sentinel__ = object()

      def __init__(self, obj = __sentinel__):
        self.isNothing = False
          if obj == self.__sentinel__:
            self.isNothing = True
          else:
            self.just = obj

    def just(x): return Maybe(x)
    def nothing(x): return Maybe()
Now we can have 'mypop' and 'buy' to return Maybe types to cover the cases of empty lists and closed markets.

    def mypop(l):
      if len(l) > 0:
        return just(l.pop())
      else:
        return nothing()

    def buy(tendered):
      effective = tendered*1.2 # there's a discount!
      if market_open():
        if effective > 60:
          return just(True)
        else:
          return just(False)
      else:
        return nothing()
So now we've abstracted the 'None's out a bit. 'mypop(l)' is always a "Maybe(Bool)" and you can inspect it to determine what the case is like 'mypop(l).isNothing'. Of course, this isn't any different from using None's and checking with "x == None" except it's more explicit: if you forget that 'mypop' returns Maybes then your program will throw an error on every use... instead of just the ones where you could have gotten a None without noticing for a while. (This is where the "You Probably Already Invented Monads" idea comes from, btw.)

In Haskell we'd say that mypop has type

    mypop :: [Float] -> Maybe Float
and buy has type

    buy :: Float -> Maybe Bool
but this makes it harder to make 'trybuy :: [a] -> Maybe Bool' since buy doesn't accept Maybes. It's also pretty clear, though, that if we send in an empty list we shouldn't even attempt to buy anything (our paycheck hasn't come in yet), so let's write that as a failure mode.

    def bind(maybe_something, maybe_process):
      if maybe_something.isNothing:
        return nothing()
      else:
        return maybe_process(maybe_something.just)
and now we can write trybuy such that it respects maybes with almost no additional noise

    # trybuy :: [Float] -> Maybe Bool
    def trybuy(l): return bind(mypop(l), buy)
The pattern we've captured here---the creation of lots of explicit context to help make functions more total and fail more quickly if you misuse them and the use of 'bind' to write functions that don't need to know about context on their input---is the Monad pattern. In Haskell, we recognize that many, many kinds of context are Monadic and thus can have default compositions programmed in. 'bind' is heavily overloaded and whenever you're writing code with context you use it often to compose functions without paying the complexity cost of routing that context around.

It's obviously not a really great Python pattern. This largely has to do with the fact that Python isn't terribly explicit about what's going on: you have to remember where exceptions and edge cases can fall out. In Haskell, we just use types to encode all of that into the documentation and the type checker ensures that we never mess up.

1 comments

Thanks for the lengthy reply!

So Monads are a complicated error-propagation framework that has a harder-to-understand conceptual model and results in longer code compared to Python's exceptions.

Look at any serious program in the C language -- there are enormous amounts of code devoted to error handling.

Look at the problem with Java's checked exceptions. The other day I needed to call a static method reflectively. This is what I ended up with:

  // java code to invoke a named method on a named class
  String the_class_name;
  String the_method_name;

  try
  {
     c = Class.forName(the_class_name);
     result = (Value) c.getDeclaredMethod(the_method_name).invoke(null);
  }
  catch (ClassNotFoundException e)
  {
     throw(new RuntimeException(e));
  }
  catch (IllegalAccessException e)
  {
     throw(new RuntimeException(e));
  }
  catch (IllegalArgumentException e)
  {
     throw(new RuntimeException(e));
  }
  catch (InvocationTargetException e)
  {
     throw(new RuntimeException(e));
  }
  catch (NoSuchMethodException e)
  {
     throw(new RuntimeException(e));
  }
  catch (SecurityException e)
  {
     throw(new RuntimeException(e));
  }
The point that Python's Exception model gets right, that so many other languages get wrong, is that by default, you want to pass errors to the caller.

You don't want to pass errors through the same pipeline that results flow through, since then every joint in the plumbing needs error checking. The very name "exception" is chosen to denote an "extraordinary control flow" specifically for error handling.

No, of course not. Error handling is just one example. The monad interface makes handling that problem you mention---wiring errors through the value pipeline---much easier.

And the monad part is only just the "just" and "bind" functions. Everything else is about trying to make Python more explicit with its errors: "explicit it better than implicit", right?

Monads allow for easier composition of "values in context". That context might be error, or nondeterminism, or continuation, or state, or logging, or parsing, or parallelism, or simulation, or probability, or graphs, or prolog-like logic, or streaming, or many combinations over the previous.

And in every case, you have the same interface.

If your only standard for utility is applicability to Python there's no point discussing monads or anything else that comes up in conversations about languages besides Python. Maybe the reason you haven't seen a cogent explanation of monads using only Python is the same as the reason you haven't seen a cogent explanation of how to use the clutch in an automatic car, or how to desalinate fresh water, or how to make lemonade with nothing but a jug and ice water. Maybe the problem isn't the explanation or the one producing it.