Inventing Monads
blog.kabir.sh
blog.kabir.sh
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 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...
Good job is what I mean
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!
I think I need a UnderstandInTheFuture Monad in my head that maintains the state of my understanding. :D
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.
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 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.
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.
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].
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.
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.
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.
That's the simplest, most understandable and most accurate explanation for an initial intuition I've come up with so far.
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.
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.You can understand flatMap and still not really understand "Monad", just as you can understand array iteration and still not necessarily understand "Iterator".
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.
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.
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.