Object-oriented – Are monads a viable (maybe preferable) alternative to inheritance hierarchies

monadobject-oriented

I'm going to use a language-agnostic description of monads like this, first describing monoids:

A monoid is (roughly) a set of functions that take some type as a parameter and return the same type.

A monad is (roughly) a set of functions that take a wrapper type as a parameter and returns the same wrapper type.

Note those are descriptions, not definitions. Feel free to attack that description!

So in an OO language, a monad permits operation compositions like:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

Note that the monad defines and controls the semantics of those operations, rather than the contained class.

Traditionally, in an OO language we'd use a class hierarchy & inheritance to provide those semantics. So we'd have a Bird class with methods takeOff(), flyAround() and land(), and Duck would inherit those.

But then we get into trouble with flightless birds, because penguin.takeOff() fails. We have to resort to Exception throwing and handling.

Also, once we say that Penguin is a Bird, we run into problems with multiple inheritance, for example if we also have a hierarchy of Swimmer.

Essentially we're trying to put classes into categories (with apologies to the Category Theory guys), and define semantics by category rather than in individual classes. But monads seem like a much clearer mechanism for doing that than hierarchies.

So in this case, we'd have a Flier<T> monad like the example above:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

…and we would never instantiate a Flier<Penguin>. We could even use static typing to prevent that from happening, maybe with a marker interface. Or runtime capability-checking to bail out. But really, a programmer should never put a Penguin into Flier, in the same sense they should never divide by zero.

Also, it's more generally applicable. A flier doesn't have to be a Bird. For example Flier<Pterodactyl>, or Flier<Squirrel>, without changing the semantics of those individual types.

Once we classify semantics by composable functions on a container — instead of with type hierarchies — it resolves the old problems with classes that "kind of do, kind of don't" fit into a particular hierarchy. It also easily & clearly allows multiple semantics for a class, like Flier<Duck> as well as Swimmer<Duck>. It seems like we've been struggling with a impedance mismatch by classifying behavior with class hierarchies. Monads handle it elegantly.

So my question is, in the same way that we've come to favor composition over inheritance, does it also make sense to favor monads over inheritance?

(BTW I wasn't sure if this should be here or in Comp Sci, but this seems more like a practical modelling problem. But maybe it's better over there.)

Best Answer

The short answer is no, monads are not an alternative to inheritance hierarchies (also known as subtype polymorphism). You seem to be describing parametric polymorphism, which monads make use of but are not the only thing to do so.

As far as I understand them, monads have essentially nothing to do with inheritance. I would say that the two things are more-or-less orthogonal: they're intended to address different problems, and so:

  1. They can be used synergistically in at least two senses:
    • check out the Typeclassopedia, which covers many of Haskell's type classes. You will notice that there are inheritance-like relationships between them. For instance, Monad is descended from Applicative which is itself descended from Functor.
    • data types that are instances of Monads can participate in class hierarchies. Remember, Monad is more like an interface -- implementing it for a given type tells you some things about the data type, but not everything.
  2. Trying to use one to do the other will be difficult and ugly.

Finally, although this is tangential to your question, you may be interested to learn that monads do have some incredibly powerful ways to compose; read up on monad transformers to find out more. However, this is still an active area of research because we (and by we, I mean people 100000x smarter than me) haven't figured out great ways to compose monads, and it seems like some monads don't compose arbitrarily.


Now to nit-pick your question (sorry, I intend for this to be helpful, and not to make you feel bad): I feel there are many questionable premises which I will attempt to cast some light on.

  1. A monad is a set of functions that take a container type as a parameter and returns the same container type.

    No, this is Monad in Haskell: a parameterized type m a with an implementation of return :: a -> m a and (>>=) :: m a -> (a -> m b) -> m b, satisfying the following laws:

    return a >>= k  ==  k a
    m >>= return  ==  m
    m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h
    

    There are some instances of Monad which are not containers ((->) b), and there are some containers which are not (and can not be made) instances of Monad (Set, because of the type class constraint). So the "container" intuition is a poor one. See this for more examples.

  2. So in an OO language, a monad permits operation compositions like:

      Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
    

    No, not at all. That example does not require a Monad. All it requires is functions with matching input and output types. Here's another way to write it which emphasizes that it's just function application:

    Flier<Duck> m = land(flyAround(takeOff(new Flier<Duck>(duck))));
    

    I believe this is a pattern known as a "fluent interface" or "method chaining" (but I'm not sure).

  3. Note that the monad defines and controls the semantics of those operations, rather than the contained class.

    Data types that are also monads can (and nearly always do!) have operations that are unrelated to monads. Here's a Haskell example composed of three functions on [] which don't have anything to do with monads: [] "defines and controls the semantics of the operation" and the "contained class" doesn't, but that's not sufficient to make a monad:

    \predicate -> length . filter predicate . reverse
    
  4. You have correctly noted that there are problems with using class hierarchies to model things. However, your examples don't present any evidence that monads can:

    • Do a good job at that stuff that inheritance is good at
    • Do a good job at that stuff that inheritance is bad at