9 ms·
I've noticed the same thing - Haskellers are terrible at describing them. I'm not at all a Haskell expert, but I think I understand enough of Monads to explain
by BoiledCabbage 10y ago
I've noticed the same thing - Haskellers are terrible at describing them. I'm not at all a Haskell expert, but I think I understand enough of Monads to explain them.
Yes they are as simple as people claim they are, and if you've done any functional programming you may already know them.
1.
Imagine a datastructure and a value. In this case we'll use "List" as the datastructure, and "Int" as the value.
So [3,4,5]. It's type is List<Int>.
2. Next we'll take "f" as a function that converts a single value to a list of values (ie a datastructure of our values). In this case the function takes a value and outputs a list of 3 copies of the value.
ex. f(8) = [8,8,8]
The type of f is: Int -> List<Int>
// Takes in an int, and returns a list of ints.
Also imagine a function "g" that does the same thing but makes 5 copies of the data.
ex. g(7) = [7,7,7,7,7]
It has the same type as f: Int -> List<Int>
3. How we apply the function to the data:
Frequently when we have lists and functions over their values we will call map over them.
_.map(f, [3,4,5]) = [[3,3,3],[4,4,4],[5,5,5]]
Notice that we don't get out a list of ints (type: List<Int>), instead we have a list of lists of Ints (type: List<List<Int>>).
We also often like to create/compose chains of these functions to create a pipeline to transform the data. So normally we would want to do something like:
r1 = _.map(f, [3,4,5])
r2 = _.map(g, r1)
But we can't. r1 is of the wrong type to pass into the second map. r1 is a List<List<Int>>, but the g is expecting a List<Int>. So how do we fix this.
How do we turn
[[3,3,3],[4,4,4],[5,5,5]] into a flattened list of ints? We append them.:
[3,3,3] ++ [4,4,4] ++ [5,5,5] // where ++ is list append in this language
the end result is [3,3,3,4,4,4,5,5,5]. Which is perfect. This is now something that we can pass into the _.map(g, ...) above.
We first 'mapped' transforming List<Int> -> List<List<Int>>. Then we "flattened" so that List<List<Int>> became just List<Int>. This allowed us to run map with g next.
This combination is bind.
Now the absurd part. If you've used something like C#'s linq there is a function that does this. It's called "SelectMany()". SelectMany is bind. It converts each element in a list to a new list, then flattens and appends the nested lists into a single list.
Bind is nothing more than SelectMany().
The only notable piece is that bind is abstract. There are different incarnations of bind for each datastructure. SelectMany is the bind for Lists. There is a different implementation of bind for "Maybe" type. Or other datastructures.
And a Monad is nothing more than a datastructure that supports SelectMany/bind. List is the one that you use all the time. The selectMany concept generalized to any datastructure is the concept of bind + a Monad.
- jnordwick 10y agoThis seems too simple. Then what is all the business of using them to contain side effects, like the IO system? Others talk about controlling program flow. What you described is flatMap in Java, or join() in other languages. Somebody above went into the CPS transform of a program. I'm so lost at how simple this is versus how complex all the descriptions are.
- gizmo686 10y ago>This seems too simple. There is a reason people say monads are simple. The comment you are replying to is describing the concrete example of the List monad. To answer you direct questions, it is probably better to look at another example: the State monad. The idea behind state is simple. Suppose you have a bunch of functions that you want to call in sequence. This is a simple enough problem; just do f(g(h(x))) or `h(x) |> g |> f` [0]. Now, suppose that, in addition to piping a value from one function to the next, we want to keep track of additional state. For example, maybe we want to use a psuedo-random number generator in some of the functions and have to keep track of its internal state. One way to do that would be to simply modify each function so that the returned value includes the state; but this quickly becomes unwieldy. The State monad provides a way to automatically thread the state through the functions. So we can do: put initSeed >>= h >>= (get \currentSeed -> g currentSeed) >>= (get \nextSeed -> f nextSeed) or (with a bit of syntactic sugar: put initSeed h currentSeed <- get g currentSeed nextSeed <- get f nextSeed in this example g and f would be expected to call put to update the internal state. Now, suppose you want to keep applying a function until some condition becomes true about the state. In this case, it makes sense to talk about a while loop: while (get >>= \state -> state =/= 7) f The above snippet will repeatadly bind f until the internal state is 7. It turns out, that we can implement while in terms of only the Monad interface: while condition action = do x <- condition if x then (action >> (while condition action)) else return () It turns out that most control structures can be encoded into monads in this way. The IO system is a bit hacky. The idea is that the universe is a big state machine. So, when we do `println "Hello World"`, we are not causing a side effect, we are just updating our internal state to say that we printed "Hello World". Simmilarly, getLine just peaks into our internal state to see what the input at the current time has always been. Of course, for some reason, GHC does not give us the full get or put functions for the IO monad; but they could exist in theory, and that is enough to pretend that side effects do not exist. [0] Not actual haskell because we can't agree on a name for the pipeline operator I am calling "|>" here.