Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Monads Explained Quickly (breck-mckye.com)
64 points by Lazare on April 9, 2016 | hide | past | favorite | 39 comments


Stop explaining monads. There are two ways to approach them. One is by doing. The other is by math. If you can't understand how "monads are monoids in the category of endofunctors", you should try learning by doing.

Don't try reading about it, don't try reasoning about it. Just do it.

EDIT: okay, I'll explain. Haskell et al. have this really neat propensity to abstract well. That means that very many concepts (Promises, null types, etc.) can actually be abstracted into one concept (the Monad) because they all have the same behavior except for a couple of points. You won't believe me, but that's exactly it.

To explain "what monads are" you must explain everything (or most things) that monads can do. Save yourself the time. Don't read or write such a thing. They're so general and so abstract as to be totally meaningless without understanding what they're built on (either in the PL/doing direction or in the mathematical/conceptual direction).


Well, that oft-maligned definition is actually what made it stick for me . . .


Right, the math does work for some people! But you have to have a mathematical background that's fairly hard to come across (category theory is pretty far from most people's reach and rarely applied except in PL and algo study)


>The other is by math. If you can't understand how "monads are monoids in the category of endofunctors"

I don't think you need to understand that in order to have a more abstract understanding of Monads.

Just go read the Monad typeclass definition (EDIT: and consequently the Applicative and Functor definitions). If you don't understand what it means, learn about higher kinded types and typeclasses until you do.

Once you understand what the definition means, that's it. No really, that's it. And now, since you're probably wondering what all the fuss is about, that's when you need to start learning by doing.


"Monads Explained Quickly, But Mostly Incorrectly" would be a better name.

Isn't what's defined here mostly just a functor?


Half of a functor and half a (co)monad; it's very misleading and will just increase the general confusion around monads for beginners.

Even when you think you figured it out, it's just one part of the puzzle too. I'd stay away from anything self-described as "quickly" or "the hard way".


Some things are easier to learn by doing. These exercises (https://mightybyte.github.io/monad-challenges/) are what made it stick for me.


This article is almost entirely wrong


Everyone here is saying the article is wrong, but no one really adds anything or corrects the author.

Can you expand?


The problem with monads is that truly understanding monads renders one completely incapable of explaining monads to anyone uninitiated. This seems to be the most fundamental property of the monad.

It seems, from the plethora of monad explanation articles that get posted here, that many people, in their hubris, think they've learned monads and think that they will succeed where so many others have failed and come up with the first approachable explanation of monads. These explanations fall into one of two categories, either the explainer has truly learned monads and offers up an explanation that's correct but entirely confusing to anyone who doesn't already understand the concept. Or, as in this case, the explainer hasn't actually understood monads and offers up an explanation that is, indeed, approachable to someone learning monads but is, never the less, incorrect.

In these cases, it does no good for those who understand monads to try to explain where the explanation fails. Because as people who understand monads, their explanation will surely cause far more confusion than it will address. It is therefore all that they can do to simply point out that the explanation is wrong and that we're still waiting for the one true approachable way of learning monads.


I feel like you're overstating the problem, and I think that in itself is a good portion of the problem to start with.

Monads have acquired this mystique that actively makes them harder for people to understand, because they don't have enough moving parts to satisfy people's expectations of a difficult concept. More often than not, the reaction of someone finally understanding monads is "Is that it?", and the answer is yes.

There exist decent two reasonable pedagogies for learning monads, and good examples of them. The first is to teach people all the prerequisite concepts (HKT, typeclasses, Functors and Applicatives) properly, and then demonstrate a case where they're not quite enough, and you need a monad. Learn You A Haskell is a good example of this.

The other is to discuss different examples of monads in non-monadic terms until people start to see the common pattern between use cases. As I understand it, this is the approach taken by You Could Have Invented Monads.

And the final ingredient is just perseverance. I think at some point, all you can do is keep working until it clicks. Perhaps we'll eventually work out a pedagogy which avoids this, but for now all we can do is try to convince people they'll get it if they keep trying. Lots of people who don't consider themselves mathematical geniuses have managed it.


>"The problem with monads is that truly understanding monads renders one completely incapable of explaining monads to anyone uninitiated"<

This implies to me that no-one should be using them in code that is meant to be read by others


Firstly the article says a monad needs to have a way to extract the value out of it. This is patently false. The most famous monad of all, the IO monad doesn't have a way to extract anything from it (notwithstanding evil trickery). Secondly the article says it needs a way to map a function over it. This is a functor not a monad. Being chainable is not overly important, as the functor law ensures that chained applications of fmap can be coalesced into a single one.


I'd prefer to use the Maybe monad as an example of not being able to extract a value, as it's far clearer that sometimes there is no such value.

>Secondly the article says it needs a way to map a function over it. This is a functor not a monad.

Yes, but since monad is a superclass of functor, it's still a necessary property. Certainly, I'd much prefer it be taught in that order, but this is definitely a property you need to understand in order to form intuitions about monads.


I don't know how to expand. The article is entirely wrong. I can't explain in a comment what's wrong with it because everything is wrong.

This article would barely be any less correct if it gave a recipe for pico de gallo and explained that that recipe defined a monad.


I appreciate the enthusiasm and the intent, but this is misleading. The maybe monad can be a wonderful abstraction, however the way used makes me think of something that can be expressed with applicative functors.


There are so many attempts at describing what monads are, but so few attempts at teaching why you would want to use a monad, and where it would be beneficial over other coding styles.


At risk of adding to the pile of half baked pedagogy, here's my take at why they're useful. The operation to chain things together (denoted >>= and pronounced bind) in the Maybe monad has type

   Maybe a -> (a -> Maybe b) -> Maybe b
In a more a java-like notation, this is like

  Maybe<B> bind(Maybe<A>, Function<A, Maybe<B>>)
The Haskell notation says that it's a function that takes two arguments (separated by ->). The first argument (Maybe a) is either something of type a or Nothing. Nothing is like null. The second argument (a -> Maybe b) is a function of one argument that takes a thing of type a and returns either a thing of type b or Nothing. The Maybe monad defines this chaining function for you. It uses a function you provide to turn a Maybe a into a Maybe b.

It's useful, because a popular alternative is to turn Maybe a into Maybe b by writing functions of type

  Maybe a -> Maybe b
To write a function like that, you have to handle both cases, a normal value and a null value. This is a pain in the ass and I don't enjoy it. Wouldn't it be easier to only have to write a function that handles the non-null value? If the value is null, then it should always return null anyways! Why should I have to repeat that for each step of my code? Let's let the language detect our context (the fact that we're doing a bunch of Maybe operations) handle that for us.

More generally, for monad m, >>= has type

  m a -> (a -> m b) -> m b
You can make new monads if you define this function for it (and another function that's easier but I won't talk about). The reason it's worth doing is because it's easy to write code that handles simple values and returns complicated things (a -> m b), but hard to write code that takes complicated things and returns complicated things (m a -> m b). With monads, you write the complicated version once and then write simple code every time you use it.

It's no silver bullet, but it turns out to be a common pattern worth abstracting when you have structured data that you have to unpack and deal with in the same way over and over again. It's flexible too, but I won't go into that.


For the "maybe a" case, you then need to take all of your code and place it in some machinery that knows what "maybe" means, and how to extract "a" from it.

And then you need to write or convert all your functions to return maybe values rather than just plain values.

All of this could be easily done with exceptions. Why do all that when something simple like an exception will do, is easily understood, and easily coded?

If there are other common usage patterns where monads are great fit, I'd love to hear it. But, given the examples of usage I've heard about, they seem to be a somewhat painful mechanism which are useful only in a few obscure cases.

Admittedly, I do not understand category theory, so I can accept that I might not be able to see the benefit unless I do.


You don't need to "place it in some machinery that knows what 'maybe' means, and how to extract 'a' from it". Because all these are defined in the standard library. If you don't like relying on the standard library you can easily implement this "machinery" with five lines of code (the Maybe definition and its Monad instance).

It's all about being explicit. In many languages, even statically typed ones, like C++ and Haskell, exceptions aren't reflected in the type system. This makes it easy to forget to handle an exception.

If you are not convinced about the utility of Maybe monads, maybe you can find other monads that aren't as easily mapped to other concepts in imperative languages, like the various parser monads, or just the List monad (which can be used to implement a simple parser).

You don't need to understand category theory. Learning category theory is only just something nice-to-have; it can help you devise new and novel abstractions, but hey, perhaps 99% of the code don't really need new abstractions.


It can still help save you time where you would normally use an exception. If I only want to continue the current operation if no exception is thrown, I can use a function I've written without any exception handling by using a functor:

    return readFromFile("file.txt").map(text -> parse(text));
In this case, I'm only going to parse the contents of the file if they exist. Normally, I'd have to write:

    try {
      return readFromFile("file.txt");
    } catch (SomeFileException e) {
      // Handle it
    }
Of course, the top one lets you handle it up-stream, but the specific thing about using types instead of exceptions is that you're forced to handle it at some point, but that doesn't mean you have to write substantially more error-checking code.

Now with a monad, I can do the same kind of abstraction even more powerfully. Say I have a function that may return an exception and I want to run that over the file:

  return readFromFile("file.txt").bind(text -> writeToFile("some_other_file.txt", text);
In this case, writeToFile could fail. If it does, it's going to return Nothing. Otherwise, it returns Just (the text we wrote). Now, if we were to just use .map() here, we would end up with Maybe<Maybe<String>>. We don't want that, that's redundant information. It either failed, or it didn't. In order to help us out, the .bind() function consolidates the two Maybes into one Maybe, so we get back a Maybe<String>. Here is where monads really pay off: with exceptions, we'd have to handle them at multiple levels, but using .bind() we're able to only handle them once (by checking in our main function, for example, what we got back from the entire process).

If we read the definition of bind here, it's a lot more clear (here I'm specializing the return types for more clarity, but in Haskell it'd be a more generic definition):

    Maybe<String> bind(Function<String, Maybe<String>> f, Maybe<String> x) {
      switch (x) {
        case Just(theString):
          // Notice, we're not returning Just(f(theString)).
          // We let f() determine whether we return back something
          return f(theString);
        case Nothing:
          return Nothing;
      }
    }


Yeah sure, I'm in the mood to write, but didn't want to cram too much into one post, else risk muddying the message. It turns out that doing (a -> m b) instead of (m a -> m b) can still allow really cool control flow. But first let me address some of your points.

>For the "maybe a" case, you then need to take all of your code and place it in some machinery that knows what "maybe" means, and how to extract "a" from it.

This isn't too bad, since somebody else wrote all that machinery. It's just a nice simple library that is super flexible, and integrates well with everything around it (in haskell at least).

>And then you need to write or convert all your functions to return maybe values rather than just plain values.

I was worried people might think that. The other function besides bind needed to make a monad is called "return" or "pure." It's a function of type a -> m a. For Maybe, it just changes the type from a to Maybe a and retains the value. The constructor is called Just, because it takes 1 and returns Just 1. It's like a default way to stick a pure value into this monad. You can compose this with any of your regular functions instead of writing or converting your nice pure (a -> b) functions. So if I have a pure value like 1, and want to apply a pure function f that just adds 2, but need to return a Maybe, then I can use return, as in "return (f 1)". Now it returns the maybe type. With javascript syntax lambda functions, using bind, I can write

  bind(Just 1, one => {return (one + 2)})
or in haskell lambda syntax (\x -> f x) with the infix version of bind (>>=)

  Just 1 >>= \one -> return (one + 2)
That way, I didn't have to convert my addition function + to return maybe values (maybePlus or something), I just used it with return.

It is even more useful with two variables:

  Just 1 >>= \one ->
  Just 2 >>= \two ->
  aNullableFunction one >>= \x ->
  return (aNonNullableFunction two) >>= \y ->
  return (one * y + two * x)
Hopefully that convinces you that you don't need to convert your functions, like aNonNullableFunction, +, or * . Additionally, haskell has syntax sugar to make that chain of functions much prettier:

  do
    one <- Just 1
    two <- Just 2
    x <- someComplicatedNullableFunction one
    let y = someNonNullableFunction two    -- could have written "y <- return (someNonNullableFunction two)" in this case
    return (one * y + two * x)
>All of this could be easily done with exceptions. Why do all that when something simple like an exception will do, is easily understood, and easily coded?

It's just easy to explain with simple examples that can be dealt with other ways. By no means is it restricted to simple tasks. Random number generation is a fun one. In python, if you were determined to avoid global state for random number generators, you might have all your functions take a seed, like

  (x, newSeed) = randomUniform(seed)
  (y, newerSeed) = randomNormal(newSeed)
  return (x*y, newerSeed)
while in the haskell equivalent, you can recognize the common "takes seed -> returns value and new seed" and let all that passing around seed stuff be implicit:

  do
    x <- randomUniform
    y <- randomNormal
    return (x*y)
It's convenient because you can program with x and y as regular integers, not fancy wrapped values, but still not have to worry about passing the random number seeds around.

It's out of the scope of a comment, but the coolest case study might be software transactional memory (STM). I'm going to do what I hate in the haskell community and link a paper that explains it better than I could, but I'm getting tired. http://research.microsoft.com/en-us/um/people/simonpj/papers... The idea is that STM let's you write code where bind appears to be simple pointer magic, but actually does things behind the scenes to keep concurrency nightmares from happening while being easy to use.

And you really don't need to grok category theory. Some of the most prolific haskellers have said they don't, which gives the rest of us hope.


Lets take your first example, lets say someone creates some nifty useful function grabPiFromWebPage that returns a "JustValueOrError" (which has the result or an error message" instead of "Maybe".

If I understand it correctly (please bare with me if I make a mistake, I'm not very familiar with haskell)

  Just 1 >>= \one ->
  Just 2 >>= \two ->
  aNullableFunction one >>= \x ->
  grabPiFromWebPage >>= \w ->
  return (aNonNullableFunction two) >>= \y ->
  return (one * y + two * x + w)
wouldn't work, because grabPiFromWebPage returns the wrong type of monad. If you wanted to see the error on failure, you'd have to have some special bind for aNullableFunction to convert its monad to "JustValueOrError", as well as convert the "pure" functions.

So now, because you want to add a new kind of function to the implementation chain, you have to write some sort of converter function for both the "maybe" and the "pure" functions. It seems brittle.

Secondly, it is also confusing as to what is actually happening in the code. When I look at:

  do
    x <- randomUniform
    y <- randomNormal
    return (x*y)
I then have to go some place (where I don't know because I don't know Haskell), and figure out what is happening there to confirm that this is operating properly. And if I make a mistake doing that, I could easily get the wrong impression and possibly think, for example in this case, that code isn't deterministic.

Finally, I've also seen examples where probability distributions are calculated and complex things like that. I could easily see a combinatoric slow down that is not self evident because everything is hidden in these complicated structures.


You're right that in the case of grabPiFromWebPage returning a JustValueOrError, you'd have to apply a jvoeToMaybe function if you wanted to use Maybe's bind. It could be an issue, but in practice, everyone uses the standard library tools for these things, so this issue shouldn't really come up. The general case problem can still arise, if you wanted to mix HTTP libraries, for example. It just isn't a huge issue in my experience. YMMV.

And I'd argue that the confusion of understanding the following isn't just a haskell issue:

  do
    x <- stuff
    return (whatever x)
You just have to read the docs (or the code) and see what "stuff" and "whatever" do, regardless of whether you're in python, haskell, or whatever. In fact, one of my biggest pain points in python (my day job language) is that you can't as easily infer what "stuff" does, because you can't just look at type signatures. If I want to use randomUniform and see that it returns a Rand Double or something, I can just look up the Rand monad and see how they work. At least in a static language like haskell. Poorly documented monads would probably be a huge pain in a dynamically typed language. Even well documented ones would be tough to use in any interpreted language, because the compiler telling you "No, imh, that has to be a Rand Double, not a regular Double!" is lifesaving.

A pure language is a great match too, because practically all code is deterministic. If you write use it twice it WILL have the same result. The only exception is something in the IO monad. Of course, interacting with the real world/random numbers isn't deterministic, so (in haskell) you need to use monads to do anything. Hence the proliferation of tutorials. So if you see a function that has type (IO Double), it's a nondeterministic double. But if it doesn't have that, then you know it's deterministic, just by looking at the type signature. This is the hardest part of learning haskell.

The point about all this not being self evident because of complicated structures is well made though. There's serious cognitive overhead sometimes, but it's a tradeoff. I've quipped a few times that haskell code doesn't get more complex, it just gets harder. It's a joke, but there's an element of truth to it. You can avoid some explosions in how many edge cases there are to consider by going up a level of abstraction. For example, using functions that operate on lists, you never have to worry about index out of bound exceptions, or using Maybe you don't have to think about handling nulls. But you do have to grok the map function for List or Maybe. Sometimes it's worth it, other times it's not.


I really have to thank you for this comment. I had serious trouble understanding Haskell's type notation, and translating it to Java-ish was immensely helpful.


These are some tangible explanations of Maybe monad in C#. Was helpful for me to understand, and now using some the concepts in my projects.

http://www.codeproject.com/Articles/109026/Chained-null-chec...


Monads basically capture computation itself: it gives you a way to describe a computation in terms of smaller computations arranged in a tree. I would recommend you to take a look at the history of monad in Haskell: why Haskell's initial approach of `main :: [Request] -> IO [Response]` doesn't work, and then perhaps you will appreciate why you want them at all.


The benefit is that, by recognising a common pattern of composition among many different types, you can develop a whole suite of functions that work with anything that follows that composition pattern.

This means you can encounter an unfamiliar monadic type, and form a decent intuition for what it does based on its purpose (although obviously you should probably check). Then you can encounter a weird new data structure, and immediately start mapping monadic functions over it without learning anything about its internals.

Also, a lot of monads make it more convenient to deal with certain things more strictly (IO, checking Maybes, avoiding mutable state etc.) such that you actually do them, instead of thinking they're too much faff.


I'd suggest a longer (1 hour) but much more correct explanation. It doesn't use math beyond high (or even may be middle) school. Video, unfortunately. https://youtu.be/ZhuHCtR3xq8


Wouldn't it be (with a little runtime typechecking):

function IdentityMonad(v) {

   this._v = v;
}

IdentityMonad.prototype.bind = function(f) {

   var o = f(this._v);

   if(!(o instanceof IdentityMonad)){ 

     throw new Error("function must return IdentityMonad");

   }

   return o;

}


I would now think beginners are easily misled by Haskell's do notation. The <- syntax makes it looks like we are extracting values out of a monad, and the do notation certainly looks like it is chainable.


What language is this? It looks like Javascript, but with some additional operators, such as "=>".


Ecmascript 6 added => as a short function syntax.


Ah. I don't use Javascript that much, and have been writing obsolete Javascript.


x >>= f = join (fmap f x)

Contemplate until enlightened. But don't write YAMT afterwards.


I see monad tutorial after monad tutorial posted here, but never some new, awesome piece of technology produced using Haskell.

In fact, the only Haskell software I can think of is Darcs, a DVCS that was so buggy and slow when I first started learning Haskell in 2010 that the community recommended against using it, and of course xmonad, dons' tiling window manager that he flagrantly abused his moderator status on r/programming to aggressively promote.

Yet Haskell remains one of the most popular languages here. There are more stories about it than Perl or C#. From this I can only conclude that Haskellers are all talk and no action, and IMO, this reflects very poorly on the language and its community.

EDIT: Also, the downvotes that one swiftly and copiously collects from merely criticizing Haskell and its community also reflect very poorly on both.


pandoc, quickcheck and git-annex are the first things that came to mind.


Judging by the volume of posts here about it, there should be as impressive a portfolio for it as there is for C# or Perl. There isn't.


Mathematics is also a popular topic on HN but there isn't an impressive portfolio of projects written in that either. Pretty daming for mathematics.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: