Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Inventing Monads (kabir.sh)
140 points by tambourine_man on Aug 5, 2019 | hide | past | favorite | 40 comments


> I'm fifteen years old with good knowledge of mostly high school level math, and may have missed some parts.

Kabir,

Great work on getting this done. I wonder if looking around for and mentioning the similar "You Could Have Invented Monads"[0] would be a good move

[0] http://blog.sigfpe.com/2006/08/you-could-have-invented-monad...


I know "20y of professional practice in the industry expert"s who would look at you with a blank stare for such mastery of non-trivial abstractions, and yet not get it.

Good job is what I mean


I sometimes wonder if the "old" are just so set in their ways that they (unconsciously) don't want to learn anything new. Will has great power.


I don't know, but I have noticed two trends: for people who have worked x years, some have x years of experience, some have x times the same year.


Thanks haha, I had exposure to computers from a very early age, and I try to make functions/systems representing things I'm curious about. It's helped me grasp ideas and come up with new ones, and I should probably write about them more :P


As one of these guys (not yet 20 years experience but close), I concur. I don't accept defeat though as I'm actually trying to learn FP for the third time! I bookmarked this blog post for when I get to monads again.

Actually I'm fine with "lightweight" FP like Scheme or Elixir (HOFs, recursion, immutable data, etc.) but still struggle with "real" FP like Haskell or functional Scala (algebraic data types, monads, lenses, etc.)

By the way I couldn't help to notice that the blog also features a CSS centering article. The author really is into esoteric concepts!


for a good explanation what ADTs are useful for watch Effective ML by Yaron Minsky from 18:00 https://www.youtube.com/watch?v=-J8YyfrSwTk


Humility and self awareness is always inspiring. As well as starting to learn new concepts 20y in. Kudos!


Thank you! I'll mention "You Could Have Invented Monads" along with a couple of other great resources that helped me at the end of the post.


Great article and it started to grok for me by the end. Although that happens every time I read an article on Monads... getting it to stick in my head is the problem.

I think I need a UnderstandInTheFuture Monad in my head that maintains the state of my understanding. :D


They say the best teacher is a recent novice.


That’s interesting—I’ve noticed my ability to explain programming concepts to newcomers seems to have degraded.

I think you can maintain it by continually teaching, but in my case I had a gap of 5 years or so, and trying again afterward I feel less of an understanding of the mind of a newcomer, which is instrumental in teaching...


https://en.wikipedia.org/wiki/Curse_of_knowledge has some good sources on studies done on this concept.


> Think of monads as a way to overload a semicolon. It might sound a little crazy at first, but imagine being able to override the semicolon to reduce boilerplate in specific code blocks. That's basically how monads are used in practice.

Lol, this resonated well. It is both brilliant but also extremely confusing (talking with experience). In my previous company, when we collaborated while researching Monads, we ended up in this conclusion as well. Some even argued that Monads actually override the indentation before an expression to turn it into a statement which doesn't help at all either.


That's a pretty good explanation, thanks. Along with the monads in Clojure [0] article I read recently, I feel I'm getting a decent grasp on monads finally.

Now they're beginning to click, I don't feel they're too complicated. Simply a way to insert a converter function between each stage in a good-old-fashioned pipeline of function applications? The converter takes the output of each function, transforms it so that it can be piped into the next function, then takes that output and combines it with the previous one, etc. Please, someone enlighten me if I've got the wrong end of the stick.

[0] https://github.com/khinsen/monads-in-clojure/blob/master/PAR...


It doesn't necessarily convert. You can compose with a function and the types will line up the way you'd expect, but you can't assume that the function is always being fed a single value - the function might be called zero, one, or many times, or stored to be called later. It might even dynamically be called either now or later. I find the most common misconception is to assume that all monads are "container-like", somehow having a value "inside" them. Whereas actually it's much more general than that.

I'd recommend playing around with some non-container monads - e.g. future or continuation. Personally I found it was easier to grasp the general case once I had experience with a bunch of specific examples.


It’s also helpful to eventually learn Haskell and learn why they needed something like Monads to cleanly solve various problems given the safety and purity constraints imposed by the language.

http://cs.coloradocollege.edu/~bylvisaker/MonadMotivation/

Although that can quickly turn into an academic rabbit hole of theory so it helps to keep it connected to your real world usage instead of getting fancy. Which is why I found it helpful to learn in the context of actually using Haskell and playing with real code.


You have it right.

It is literally a container that allows you to pipeline functions on its contents. The only technical catch is that you aren't allowed to change the type of the contents in the function. That means if you start with something of type A in the container, you have to get a container containing something of type A at each stage of the pipeline.

There are probably 3 popular uses for monads. First to defer error handling. You allow type A to include some kind of error state. The you run your entire pipeline of functions, but at every stage if the container contains an error, you just return a container with the same error as output. That way, none of your pipeline stages need to check for valid input -- they just assume there were no errors before their stage.

The second main case is to defer processing until a later time. You have a pipeline of functions and a container. As long as you don't look at the result of running that pipeline, even if the container doesn't contain anything yet you can pretend that it represents your result. You can pass that along in your program, gradually appending more functions on your pipeline as you go. When you finally put something in the container, you can see what the result is. This is useful when you don't know what your result is right now, but you know what processing you want to do when you get the input (for example, if you are going to query some external system and when it returns you want to do some processing. You can go ahead and pretend to process that result and when the data finally arrives, then you can actually get the result of the computation).

The third main case is when you have a big list and you want to narrow it. For instance, you might have a list of food. You want to narrow that to a list of fruit. Then you want to narrow that to a list of red fruit. Then you want to narrow that to a list of red fruits that are berries. Instead of writing a function to do your whole query, you can separate it: A query for fruit. A query for red. A query for berries. This can make your code a lot easier to read.

There are other things you can do with monads, but that's the basics IMHO. They really are simple, but suprisingly enlightening. You might think, "This constraint that I return the same type each time is kind of annoying", but if you spend a fair amount of time with monads you will see that this allows you to make pretty amazing assumptions in your code -- which, in turn, let you do very powerful things.


" The only technical catch is that you aren't allowed to change the type of the contents in the function. That means if you start with something of type A in the container, you have to get a container containing something of type A at each stage of the pipeline."

I don't think this is correct. What doesn't change is the overall type constructor (e.g. if you start with a list, you'll still end up with a list... If you start with an IO action, you'll still have an IO action). But the type that is wrapped by the container can and usually does change. e.g. if you "map" over a list of A's, you'll get a list of whatever type your map function returns - still a list, but a list of B's, not a list of A's. fmap :: (a->b) -> [a] -> [b].


> The only technical catch is that you aren't allowed to change the type of the contents in the function

Sticking with your metaphor of container + contents, the contents absolutely can change. It's the container that has to remain the same throughout the pipeline.


Replying to my own post because I'm too late to edit and there are two replies that rightly point out my mistake :-) Thanks!


Simply a way to insert a converter function between each stage in a good-old-fashioned pipeline

Yes, but more general. Some monad tutorials describe them as "overloading the semicolon." I felt I didn't really understand them until I got the idea of computations as values.

It's much harder to motivate monads in a dynamic language. They were invented to solve problems in Haskell and it is there where they truly shine. Having said that, most of their thunder has been stolen by applicative functors (which generalize monads) these days.


You are right. Its just a simple converter function to use in a pipeline.


No, not even close.


If you can implement flat_map [0] for something, it's (probably) a monad. You might as well call monads FlatMappables.

That's the simplest, most understandable and most accurate explanation for an initial intuition I've come up with so far.

[0] https://apidock.com/ruby/Enumerable/flat_map


It needs to be associative. More concretely, if you want to have something like "do notation" that makes sense, so that you can write code like:

    x <- f(a)
    y <- g(b)
    z <- h(c)
for composition, then it needs to be the case that x.flatMap(a => f(a)).flatMap(b => g(b)) is equivalent to x.flatMap(a => f(a).flatMap(b => g(b))). Unfortunately people sometimes implement a flatMap where these are almost, but not quite, equivalent, which leads to very confusing behaviour and subtle bugs.

You also need to be able to "point" a pure value and have the equivalences you'd expect - this often ends up being important as e.g. the base case of a recursive function.


If you have a macro then something like "do notation" could be done in a superset of JavaScript, such as something like this:

  const listMonad=(m,f)=>[].concat(...m.map(f));
  const x=listMonad*>{
    let a=yield [1,2,9];
    let b=yield [3,4,5,7];
    return a-b;
  };
Such a code can then be compiled into a ordinary JavaScript code.


Sure! But note I said "initial intuition". return/pure and monad laws can be explained later, once one understands that there's really nothing magical about monads.


Of course there's nothing magic. But I think it's important to grasp the definition as a whole rather than thinking monads are "just" some subset or superset - the full definition of a monad is quite short and every part of it is vital, even if it doesn't seem so to start with.


The problem is that being "flatmappable" very strongly implies that it's a container, but it doesn't have to be. It especially privileges the list implementation over other things. It's much the same as saying "Iterator may be a bit difficult to handle, but it's basically a way of presenting the elements of an array one at a time"; it isn't that that statement is wrong, it's that it is incomplete, and really leads people to thinking that iterators can only iterate on things you concretely have in memory, when instead they can iterate on anything (integers in sequence being popular), and there's also an "advanced mode" where you can drive the iterator (frequent example: an iterator to walk over files on the disk that can be fed back to to not descend into a particular directory upon inspection) that you'll never understand if you see iterators as "a way of walking an array".

You can understand flatMap and still not really understand "Monad", just as you can understand array iteration and still not necessarily understand "Iterator".


> The problem is that being "flatmappable" very strongly implies that it's a container

Can't fully agree with that, although I see your point. "Mappable" doesn't imply a container more than Functor's fmap method implies that all functors are containers. And "flattening" something is basically Monad's join.

But still, even if "FlatMappable" has a strong container connotation, I think it's good enough for establishing a first intuition. I think that once one shows what flatMap looks like in terms of e.g. Promises, the notion that it's about arrays or containers will vanish pretty quickly.


Just want to say, this guide has one massive advantage that's usually missing: multiple monads.

Far too many tutorials I've seen in the past will pick only one to use as an example, then act as though that specific monad's functionality applies to monads in general, which just confuses the issue.


Monads are not invented, they are discovered multiple times. You invent toilets and gadgets, but every time you come across a mathematical object, if it really is that object (with proofs), then it must be the same one (a.k.a. diagram commutes).


Wow, I haven't seen a monad tutorial in over a decade. Very nostalgic post.


Good attempt, it's good thing to popularize powerfull concepts in programming, which are not as hard as most programmers think they are. However I have one big problem with this article. Code snippets are so horrible. Seems so unpractical. Perhaps because I never used functional features of Javascript?


Hey! I attempted to make the code snippets use practical concepts, but they aren't very practical for use in actual JavaScript code — the reason being that JavaScript is not a functional language and doesn't have syntactic sugar for monads.

However, I chose JavaScript because I believe it's more familiar and widespread, and it can be easier to explain a foreign concept when it's done in a language that more people feel comfortable with.


Interestingly for someone like me who has had the luxury of never having to read or write a line of JavaScript, it was all alien gobbledygook. Whatever that code was supposed to be doing was not clear to this C++ programmer.


Sorry about that haha, the style of programming in this guide is very different from C++, and it relies heavily on currying and lambda functions using next-generation JavaScript syntax. Nonetheless, I think it can offer a new perspective because JS is more comfortable than Haskell for most developers.


Oh I'm fine with all those concepts. C++ has anonymous functions, and I'm a Haskell programmer too. But whatever syntax JavaScript uses is not standard across other imperative languages.


My bad then, that's fair. JS has been deviating from C-style syntax and it really shows in this article haha




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

Search: