20

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.)

13
  • 1
    Not sure I understand how it works: a squirrel and a duck don't fly the same way - so the "fly action" needs to be implemented in those classes... And the flier needs a method to make the squirrel and the duck fly... Maybe in a common Flier interface... Oops wait a minute... Did I miss something?
    – assylias
    Commented Apr 3, 2014 at 0:30
  • Interfaces are different than class inheritance, because interfaces define capabilities while functional inheritance defines the actual behavior. Even in "composition over inheritance", defining interfaces is still an important mechanism (e.g. polymorphism). Interfaces don't run into the same multiple inheritance problems. Plus, each flier could provide (through an interface & polymorphism) capability properties like "getFlightSpeed()" or "getManuverability()" for the container to use.
    – sea-rob
    Commented Apr 3, 2014 at 0:36
  • 3
    Are you trying to ask whether using parametric polymorphism is always a viable alternative to subtype polymorphism? Commented Apr 3, 2014 at 0:56
  • yup, with the wrinkle of adding composable functions that preserve semantics. Parameterized container types have been around for a long time, but by themselves they don't strike me as being a complete answer. So that's why I'm wondering if the monad pattern has a more fundamental role to play.
    – sea-rob
    Commented Apr 3, 2014 at 2:35
  • 6
    I don't understand your description of monoids and monads. The key property of monoids is that it involves an associative binary operation (think floating-point addition, integer multiplication, or string concatenation). A monad is an abstraction that supports sequencing various (possibly dependent) computations in some order.
    – Rufflewind
    Commented Apr 3, 2014 at 15:01

2 Answers 2

15

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
9
  • 3
    Thank you! Lots for me to process. I don't feel bad -- I greatly appreciate the insight. I would feel worse carrying around the bad ideas. :) (Goes to the whole point of stackexchange!)
    – sea-rob
    Commented Apr 6, 2014 at 0:29
  • 1
    @RobY You're welcome! By the way, if you haven't heard of it before, I recommend LYAH as it's a great source for learning monads (and Haskell!) because it has tons of examples (and I feel that doing tons of examples is the best way to tackle monads).
    – user39685
    Commented Apr 6, 2014 at 2:17
  • There's` lots here; I don't want to swamp the comments, but a few comments: #2 land(flyAround(takeOff(new Flier<Duck>(duck)))) doesn't work (at least in OO) because that construction requires breaking encapsulation to get at the details of Flier. By chaining opertions on the class, the details of Flier remain hidden, and it can preserve its semantics. That's similar to the reason that in Haskell a monad binds is (a, M b) and not (M a, M b) so that the monad doesn't have to expose its state to the "action" function.
    – sea-rob
    Commented Apr 6, 2014 at 2:54
  • #1, unfortunately I'm trying to blur the strict defintion of Monad in Haskell, because mapping anything to Haskell has a big problem: function composition, including composition on constructors, that you can't do easily in a pedestrian language like Java. So unit becomes (mostly) constructor on the contained type, and bind becomes (mostly) an implied compile time operation (i.e. early binding) that ties the "action" functions to the class. If you have first-class functions, or a Function<A, Monad<B>> class, then a bind method can do late binding, but I'll take that abuse next. ;)
    – sea-rob
    Commented Apr 6, 2014 at 3:03
  • #3 agree, and that's the beauty of it. If Flier<Thing> controls the semantics of flight, then it can expose lots of data and operations that maintain flight semantics, while the "monad"-specific semantics are really just about making it chainable and encapsulated. Those concerns might not (and with the ones I've been using, aren't) concerns of the class inside the monad: e.g. Resource<String> has an httpStatus property, but String doesn't.
    – sea-rob
    Commented Apr 6, 2014 at 3:08
1

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?

In non-OO languages, yes. In more traditional OO languages, I would say no.

The issue is that most languages don't have type specialization, meaning you can't make Flier<Squirrel> and Flier<Bird> have different implementations. You have to do something like static Flier Flier::Create(Squirrel) (and then overload for each type). Which in turn means you have to modify this type whenever you add a new animal, and likely duplicate quite a bit of code to make it work.

Oh, and In not a few languages (C# for example) public class Flier<T> : T {} is illegal. It won't even build. Most, if not all OO programmers would expect Flier<Bird> to still be a Bird.

6
  • thanks for the comment. I have some more thoughts, but just trivially, even though Flier<Bird> is a parameterized container, no one would consider it to be a Bird (!?) List<String> is a List not a String.
    – sea-rob
    Commented Apr 3, 2014 at 15:25
  • @RobY - Flier isn't just a container. If you consider it just a container, why would you ever think that it could replace use of inheritance?
    – Telastyn
    Commented Apr 3, 2014 at 15:33
  • I lost you there... my point is the monad is an enhanced container. Animal / Bird / Penguin is usually a bad example, because it brings in all sorts of semantics. A practical example is a REST-ish monad we're using: Resource<String>.from(uri).get() Resource adds semantics on top of String (or some other type), so it's obviously not a String.
    – sea-rob
    Commented Apr 3, 2014 at 15:40
  • @RobY - but then it is in no way related to inheritance either.
    – Telastyn
    Commented Apr 3, 2014 at 15:42
  • Except that it's a different kind of containment. I can put String into Resource, or I could abstract a ResourceString class and use inheritance. My thought is that putting a class in a chaining container is a better way to abstract behavior than to put it into a class hierarchy with inheritance. So "no way related" in the sense of "replacing / obviating" -- yes.
    – sea-rob
    Commented Apr 3, 2014 at 15:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.