Isn't a monad effectively just an abstract wrapper for some properties that you can apply sequential transforms on? Why do people keep making out they are some form of voodoo?
Isn't a monad effectively just an abstract wrapper for some properties that you can apply sequential transforms on? Why do people keep making out they are some form of voodoo?
Ultimately I don’t think learning them has to be hard, but it can be, and it does usually require a bit of patience to let the full picture sink in.
I think even experienced people can have issues when a type has a lot of type variables. What do they all mean? It's almost like an implicit API you need to learn for that particilular type, with varying levelsnof documentation or lack thereof
I tried learning about them for fun but couldn't see many examples unless I know all of the weird haskell syntax.
The core basis that I gathered of functors is that they're like anything which is "mappable", the key part being it can be any type, and not something like a list or array. So maybe you have an object data type, and you create a custom function which makes it mappable.
The basis of monads was a functor that returns another functor, and the example I saw was a "stream". Elixir does have streams, and this "makes sense" but I wasn't super satisfied with that answer as I highly doubt all this talk of making it hard to understand is about streams.
Is there an actual example that exists where you can present the different ways to solve it? Ie... you have X data structure, and you want to get Y parts out of it while transforming Z at the same time. Method 1: no monads, Method 2: monads, or something like that?
You could write this without making use of Monads using:
f = case op1 of
Nothing -> Nothing
Just x ->
case op2 x of
Nothing -> Nothing
Just y ->
case op3 y of
...
Or you can use the Maybe monad to make this a bit nicer f = op1 >>= (\x ->
op2 x >>= (\y ->
op3 y >>= (\z -> ...)))
Or you would probably use the do notation, which is just shorthand for >>= f = do x <- op1
y <- op2 x
z <- op3 y
...Let's first think about how we would write that second example in an imperative language (loosely modeled after Java but don't yell at me if it doesn't compile):
Header parseHeader(ByteInputStream bytes) {
int size = bytes.readWord8().toInt();
Word16[] fields = new Array();
for (i : (1...size)) {
fields.add(bytes.readWord16le());
}
return new Header(fields);
}
but that's no good in FP, since we're modifying state on a byte input stream, so let's try to make it more functional (let's make some syntax allowances for that, so it will look less like Java): Header parseHeader(ByteArray bytes) {
val (size, rest) = bytes.nextWord8();
val (_, allFields) = (1...size).fold((rest, [])) { _, (currentRest, fields) ->
val (field, nextRest) = currentRest.nextWord16le();
(nextRest, fields.append(field))
}
return Header(allFields);
}
Now we avoid mutation, so the FP gods are happy, but we have to thread the state through the computation (look at how many variables we used) and it will quickly get out of hand if the structure becomes more complicated. Plus because so many of these variables have the same type, it's easy to accidentally introduce errors.The example with the "Get Header" monad has the advantage of being as simple to read as the imperative example, but purely functional. That works because the Get monad builds up a description of steps to perform, so is purely declarative. It is only executed when I later call it on some actual byte string (e.g. by "runGet parseHeader myBytes"). As an added benefit, this means that I can define the parsing logic without actually calling it. I could also write additional code that takes this representation and does other things on it instead of just running it (e.g. running it with extra logging, or doing some kind of dry-run, etc.)
> That works because the Get monad builds up a description of steps to perform, so is purely declarative.
AWESOME way to put it. Thank you...
I think this article responds well to that kind of "it's so obvious!" flash of insight: "The Monad Tutorial Fallacy", https://byorgey.wordpress.com/2009/01/12/abstraction-intuiti...
Basically, you have to build your own intuition. Examples of monads are just examples, what matters is how you reached that insight, and there's no shortcut for that; you cannot simply be told that "monads are like wrappers" (or burritos, read below). Like the article states:
> “Monads are easy,” Joe writes. “Think of them as burritos.” Joe hides all the actual details about types and such because those are scary, and people will learn better if they can avoid all that difficult and confusing stuff. Of course, exactly the opposite is true, and all Joe has done is make it harder for people to learn about monads, because now they have to spend a week thinking that monads are burritos and getting utterly confused
[...]
> What I term the “monad tutorial fallacy,” then, consists in failing to recognize the critical role that struggling through fundamental details plays in the building of intuition. This, I suspect, is also one of the things that separates good teachers from poor ones. If you ever find yourself frustrated and astounded that someone else does not grasp a concept as easily and intuitively as you do, even after you clearly explain your intuition to them (“look, it’s really quite simple,” you say…) then you are suffering from the monad tutorial fallacy.
There are so many cool monads that work differently from IO, Maybe and such types, e.g. the binary package allows me to write
data Header = Header Word8 Word16
parseHeader :: Get Header
parseHeader = Header <$> getWord8 <*> getWord16le
or for a more complicated example data Header = Header [Word16]
parseHeader :: Get Header
parseHeader = do
size <- fromIntegral <$> getWord8
fields <- replicateM size getWord16le
return $ Header fields
and then I can later run parseHeader on some binary input to parse a header, and I can do it in one go or I can run it incrementally, it really doesn't matter, because the Get monad only describes the way the input should be parsed and not the actual parsing algorithm.It's like explaining a vector space using the analogy of arrows in space and then having to explain how sets of functions are also a vector space.
In mathematics you just have to stick with the general definition and a few examples, keeping in mind that those examples are specific. Most programmers are simply not used to thinking in that way. That's why it's "some form of voodoo".
Further, much like the definition of a vector space does not make it immediately obvious that functions can be vectors, it's not obvious from looking at the definition of a monad that it allows us to do sequential IO. That's why people think it's some sort of voodoo. You have to just accept that the example obeys the definition, the example doesn't fall out of the definition.
Totally disagree. You are effectively saying you cant define anything unless you include every case, which is clearly nonsense.
> it's not obvious from looking at the definition of a monad that it allows us to do sequential IO
The transform is "transform the wrapped properties into some IO call", and all transforms are explicitly sequential.
No I'm not. I will repeat what I said again: such things are defined by their general mathematical definition, and any analogy can only speak for certain subsets of examples of that definition.
> The transform is "transform the wrapped properties into some IO call", and all transforms are explicitly sequential.
How exactly does the mathematical definition of a monad say anything about "transforms"?
I think you didn't actually read my comment.
Im not sure that it does, but a "transform" as I mean it in my basic description is applying an operation to the "wrapped" properties.
Nice shadow edit. For context, the previous comment used to consist of a single link to https://en.wikipedia.org/wiki/Natural_transformation
In the case of monads, the things that are useful which we can say about them: unit is a left identity for bind and also the right identity for bind, etc. What makes an abstraction useful, and is kind of essential in my opinion, are the laws.
These do include every case because we don't say anything about specific terms or types, just the laws of bind (in the case of monads).
I agree that people keep making them out to be some kind of mountain to climb when, after building up an intuition and learning the fundamental, it turns out they are more of a hill. However I think it takes a good mix of both the theory and application to appreciate how elegant and simple it is in the end.
class Monad m where
(>>=) :: m a -> ( a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
and these functions must obey the properties return a >>= k = k a
m >>= return = m
m >>= (\x -> k x >>= h) = (m >>= k) >>= h
Anything other than the definition I gave is an analogy that will only work for certain examples, that is, unless you can mathematically prove that your analogy is isomorphic to this definitionNot every monad has to do with IO. The oddity of Haskell is that Haskell has this IO type and every real world action has to produce an IO value, but the defining characteristic of IO isn't that it's a monad.
Now, in the IO type, we use monads to sequence I/O operations, indeed (like "read something to the keyboard, then write this same something you read).
But there are other monads that do other things! Monads always enable you to write sequential-looking code (through do notation), sure, but they don't need to execute sequentially - and most of them has nothing to do with IO!
For example, the Cont monad enables writing code in continuation-passing style [0], and it's famously difficult to grok [1]. Suffice to say that it enables using the "call with current continuation" operator from Scheme (which doesn't work on IO, but works on Cont) [2]
[0] https://www.haskellforall.com/2012/12/the-continuation-monad...
[1] https://stackoverflow.com/questions/3322540/how-and-why-does...
[2] https://stackoverflow.com/questions/20451022/how-to-interpre...
It's not helpful to tell someone their definition is wrong because it doesn't cover all possibilities. You shouldn't ask a 6-year old to define numbers and tell them they're wrong when they describe the integers. This would just bring confusion and anger to the child and makes you look like an arse. You might instead tell them it's a good explanation, but there are some numbers, like one half, that it doesn't include. So maybe we can make the description a bit bigger and include the rationals as well. This makes the child feel good because they were right about something, and also learned something and you feel good because you helped them learn. The same is true of adults.
> This makes the child feel good because they were right about something and also learned something and you feel good because you helped them learn.
I'm not going to treat them like a child just to give them the impression that they contributed. They said something that I think is inadequate, so I told them that.
If you tried to learn linear algebra by starting with the general definition of linear transformations on vectors in an abstract space, you'd get nowhere. You need a reference point in order to effectively generalize.
For example, I first came to understand functors/applicatives/monads as contexts, so the idea of a list being a monad was actually kind of weird to me.
The other problem with monads (in Haskell) is that it's sometimes far from obvious what a particular monad actually does. If I see code like this:
thing1 >>= frobify
It tells me basically nothing about what the computer will actually do, unless I have some familiarity with the types involved. You see similar issues with things like context managers in Python, and with languages that support operator overloading for user-defined types.The difference is that we all have some generally correct intuition about what "+" means, so when we see that operator is overloaded on a custom type, we can at least have an idea about what it might do. But a programmer who lacks concrete intuition about what monads do will not be able to perform the same reasoning when they see ">>=" or "join".
An analogy I might give is memory ordering constraints in C++. If somebody comes to you and says "hey I've got some concurrent code that has these requirements", it might be thoroughly reasonable to give them a recipe that uses a particular sequencing constraint and operation pattern even though you could have a much more general discussion of the nature of happens-before relationships and proofs of correctness of concurrent algorithms. But somebody's eyes might glaze over.
This is even true in mathematics. Sometimes you want to learn the full abstraction. Sometimes you want recipes that let you solve a PDE.
The general intuition that makes sense for me after a few years of full time haskell is that Monads are the thing that allows us to encode effectful computations within a pure language.
Additionally, even if you insist that this reader monad is a "wrapper", your definition would also informally apply to things like `Applicative`, and not all applicatives are monads (some are).
Some monads are more easily understood as wrappers (`Maybe`, `List`, ...) and you can be very productive if you just stick to that level of understanding. But there's a deeper understanding that one can get.
[1] https://hackage.haskell.org/package/mtl-2.3.1/docs/src/Contr...
A lot of the "it's so hard" BS is from people who think you have to understand a concept in its full generality in one shot. That's definitely not how effective learning works in most cases.