The definition of what a monad is in your post is wrong. A monad is a type constructor, e.g., List, in a language where List is not a type, but List[int] is, together with three functions
map : (A -> B) -> (List[A] -> List[B])
pure : A -> List[A]
join : List[List[A]] -> List[A]
such that
Given
f : A -> B
g : B -> C
xs : List[A]
the following equality holds:
map (g . f) xs = map g (map f xs)
where g . f denotes function composition:
(g . f) x = g (f x)
Given
xs : List[A]
the following equalities hold:
xs = join (pure xs)
xs = join (map pure xs)
Given
xs : List[List[List[A]]]
the following equality holds:
join (join xs) = join (map join xs)
Of course, List is just an example. (Exercise: Figure out what pure and join should be for the List monad.) To implement another monad, say, Option, you have to replace all occurrences of List with Option in the type signatures of map, pure and join. That is,
map : (A -> B) -> (Option[A] -> Option[B])
pure : A -> Option[A]
join : Option[Option[A]] -> Option[A]
In other words, the only path to understanding monads is to read the fine source, fire up GHC, and write some code. Analogies and metaphors will not lead to understanding.
@tomjakubowski my mind jumped to convolutions after seeing Fourier transforms, and a convolution is an operation on two functions to produce a third function which serves as a translation between one method to another. I think the declaration of a convolution would map to a functor.
I’m reading Wikipedia and my brain is melting from the definition of the convolution theorem, but it basically says you can transform convolution in one domain into multiplication into another domain. I guess that’s akin to binding (or using some operand to restructure a monolithic calculation into a pipeline), which is a property of monads.
Theorems are about what logically follows from definitions, not about what you or anyone else “can” do. In particular, the convolution theorem asserts that, given two absolutely integrable functions f and g, the Fourier transform of their convolution ℱ(f ⋆ g) equals the ordinary product of the Fourier transforms of the original functions ℱ(f) ℱ(g). Provided we accept the usual definitions of “Fourier transform” and “convolution”, the convolution theorem is simply true. It would still be true even if nobody had been smart enough to prove it.
Convolutions are not “translations”. The easiest way to think of convolutions is as follows. Suppose you have two independent random variables X and Y that could be either 0, 1, 2, 3, with some given probabilities. Then X + Y could be any of 0, 1, 2, 3, 4, 5, 6. But, what is the probability that X + Y equals, say, 4? We need to take into account every way in which X + Y = 4 could happen, namely:
X = 1 and Y = 3
X = 2 and Y = 2
X = 3 and Y = 1
What are their respective probabilities? Since X and Y are independent,
We had a nice finite sum, because our space of possibilities is finite. When X and Y may take arbitrary real numbers as values, the sum becomes an integral.
Finally, none of this has any relationship to monads or monadic bind whatsoever.
Honestly I’m sorry I ever brought it up. I too understand both and cannot remember why that off-handed explanation made sense to me, I’ve been trying to remember all day.
There is no need to be sorry. This just means that you have to be careful with other people’s claims. If you do not understand why something has to be true, it is healthy to doubt it, at least a little. The stronger a claim is, the more you should doubt it. And the claim “This complicated thing becomes super simple if you just use this trick or analogy!” is super duper strong.
It’s not like that at all. It’s not a stupid trick (type theorists hate her!), it was an off-the-cuff comment that triggered my synapses to wire up correctly and turned my knowledge to understanding.
@WilhelmVonWeiner thanks for replying! Those are great points you raised, and I talked a little bit about them in my blog post, but to respond to you directly:
I would agree with you that crashing is the appropriate solution for most OLTP workloads (esp. those where the runtime is accessible by the developer). Worst case scenario, you lose one record, but you get a crisp error handled by the runtime. But for OLAP workloads it may be a bit different. In my last job I was handling 10^8 / 10^9 records for a OLAP job, and the tool was a monolith. Crashing may mean halting job execution or losing context of runtime state. You can’t crash on one “0” input if you have many records to process. I think crashing would also be inappropriate because the execution path differs based on the current job context (sometimes a 0 is okay, sometimes not). It was also an on-premise tool, so high-latency customer feedback loops was the only way I could resolve bugs as opposed to SSHing into a server.
Compile-time guarantees like static typing are really nice, and I’m not terribly familiar with them at the moment (I’m working through a Haskell book so I should have more knowledge by the end of the year), but I don’t see how that translates to correctness guarantees at runtime. What if you wanted to break out a monolith into services and each portion communicated to another through a protocol (e.g. stdin/stdout/stderr)?
Yeah, you can think of monads as a convolution! And thank you for the like :)
Regarding 2.: The general idea is that when you parse data from the outside world (like stdin) you don’t just validate if the input is correct but you also transfer the data into your type safe data model. That’s how the compile time guarantees also hold at runtime. You would normally use some kind of parser for this, like aeson for JSON or ReadP for general text processing.
Got it. I’m guessing since Haskell makes it easy to define types or typeclasses, there shouldn’t really be a situation where an external type could not map to a Haskell type, or if there is, there’s a catch-all bytes type acting as a container of bytes?
IIRC I encountered some weird issues with various binary encodings where I found it difficult to map to any data type in Python without being forced to apply an encoding (and then having to strip out that encoding during export). For example, reading PostgreSQL bytea into a binary column in another database using a custom process, or capturing and rendering Java log4j stdin within a parent Python process. If Haskell was flexible enough to design parsers for arbitrary bytestreams from various protocols that would be a major selling point to data engineers.
Sure, you can leave parts of your data in a more general data structure. Of course you can’t interact with these parts as easily, but if you just want to pass them through that’s ok. E.g. in aeson there is the Value type which is just a sum type over all possible JSON elements. This is still perfectly safe, because if you want to work with these values you always have to consider every possible case of the sum type. And of course there is stuff like ByteArray.
I think that syntax sugar for the bind operation is extremely important. Also, with static types tou Just can not not handle an effect; monads are not only a useful abstraction for composability but also for keeping track of effects.
I hate how people pride themselves with “X isnt as hard” and then go on about it.
The problem with the whole not-understanding-monads is because their applicability is extremely general.
On top of that tons of people randomly started praising using them (???) ~5 years ago.
I like the explanation given by experts in function programming the best. Monads are defined by their laws and nothing more. From these laws you can apply monads to a ton of problems. A lot of the time, you don’t even need to think you are using “monadic code”. It’s such a general thing people are using it all the time. I think that’s why it’s pointless as hell to a lot of people, and they “just dont get it”. It’s like being super confused about why people are losing their minds over strawberries are red. They are just red because that’s how they naturally occur for some very natural reasons. Well same for monads really. They just naturally occur.
Eventually though you will get the person who is interested in this generality. And usually these people “get it” straight away. Because they’ve seen the pattern many times over. And then they learn why all red objects in the universe are red.
I want to finish with: nothing is difficult when you understand it. Understanding is the difficult part in a lot of cases.
Can someone clarify me modan/exception difference?
So in modan we’ll get Nothing at top level. And if we catch - Exception. Only Exception also contains stack trace which is more useful, then just “nothing”.
The definition of what a monad is in your post is wrong. A monad is a type constructor, e.g.,
List
, in a language whereList
is not a type, butList[int]
is, together with three functionssuch that
Given
f : A -> B
g : B -> C
xs : List[A]
the following equality holds:
map (g . f) xs = map g (map f xs)
where
g . f
denotes function composition:(g . f) x = g (f x)
Given
xs : List[A]
the following equalities hold:
xs = join (pure xs)
xs = join (map pure xs)
Given
xs : List[List[List[A]]]
the following equality holds:
join (join xs) = join (map join xs)
Of course,
List
is just an example. (Exercise: Figure out whatpure
andjoin
should be for theList
monad.) To implement another monad, say,Option
, you have to replace all occurrences ofList
withOption
in the type signatures ofmap
,pure
andjoin
. That is,I write Haskell every single day, and I found the monad explanation in this post confusing.
Uhh… Not exactly. The first one is a monad; the others are constructors for that monad.
I don’t think trying to think of monads in terms of classical OOP is useful.
The author is right to point out though that analogies to foodstuffs and nuclear waste are also not constructive.
Want to grok monads? Here’s how:
From here.
I believe the fact that some data structures are a Monad is about as useful as the fact that integers+addition are a Group.
Totally agree. That’s why I think framing the discussion around typeclasses is the most productive way to learn.
The Python example immediately stands out to me as having two more alternatives:
(Someone told me monads were Fourier transforms but for function execution and that somehow made them click for me. I like the article.)
I think that I grok monads and Fourier transforms, but don’t see a connection. Could you elaborate?
@tomjakubowski my mind jumped to convolutions after seeing Fourier transforms, and a convolution is an operation on two functions to produce a third function which serves as a translation between one method to another. I think the declaration of a convolution would map to a functor.
I’m reading Wikipedia and my brain is melting from the definition of the convolution theorem, but it basically says you can transform convolution in one domain into multiplication into another domain. I guess that’s akin to binding (or using some operand to restructure a monolithic calculation into a pipeline), which is a property of monads.
Theorems are about what logically follows from definitions, not about what you or anyone else “can” do. In particular, the convolution theorem asserts that, given two absolutely integrable functions f and g, the Fourier transform of their convolution ℱ(f ⋆ g) equals the ordinary product of the Fourier transforms of the original functions ℱ(f) ℱ(g). Provided we accept the usual definitions of “Fourier transform” and “convolution”, the convolution theorem is simply true. It would still be true even if nobody had been smart enough to prove it.
Convolutions are not “translations”. The easiest way to think of convolutions is as follows. Suppose you have two independent random variables X and Y that could be either 0, 1, 2, 3, with some given probabilities. Then X + Y could be any of 0, 1, 2, 3, 4, 5, 6. But, what is the probability that X + Y equals, say, 4? We need to take into account every way in which X + Y = 4 could happen, namely:
What are their respective probabilities? Since X and Y are independent,
Since the three cases are mutually exclusive,
One convenient way to write this is as follows:
We had a nice finite sum, because our space of possibilities is finite. When X and Y may take arbitrary real numbers as values, the sum becomes an integral.
Finally, none of this has any relationship to monads or monadic bind whatsoever.
Honestly I’m sorry I ever brought it up. I too understand both and cannot remember why that off-handed explanation made sense to me, I’ve been trying to remember all day.
There is no need to be sorry. This just means that you have to be careful with other people’s claims. If you do not understand why something has to be true, it is healthy to doubt it, at least a little. The stronger a claim is, the more you should doubt it. And the claim “This complicated thing becomes super simple if you just use this trick or analogy!” is super duper strong.
It’s not like that at all. It’s not a stupid trick (type theorists hate her!), it was an off-the-cuff comment that triggered my synapses to wire up correctly and turned my knowledge to understanding.
Ah, okay.
@WilhelmVonWeiner thanks for replying! Those are great points you raised, and I talked a little bit about them in my blog post, but to respond to you directly:
I would agree with you that crashing is the appropriate solution for most OLTP workloads (esp. those where the runtime is accessible by the developer). Worst case scenario, you lose one record, but you get a crisp error handled by the runtime. But for OLAP workloads it may be a bit different. In my last job I was handling 10^8 / 10^9 records for a OLAP job, and the tool was a monolith. Crashing may mean halting job execution or losing context of runtime state. You can’t crash on one “0” input if you have many records to process. I think crashing would also be inappropriate because the execution path differs based on the current job context (sometimes a 0 is okay, sometimes not). It was also an on-premise tool, so high-latency customer feedback loops was the only way I could resolve bugs as opposed to SSHing into a server.
Compile-time guarantees like static typing are really nice, and I’m not terribly familiar with them at the moment (I’m working through a Haskell book so I should have more knowledge by the end of the year), but I don’t see how that translates to correctness guarantees at runtime. What if you wanted to break out a monolith into services and each portion communicated to another through a protocol (e.g. stdin/stdout/stderr)?
Yeah, you can think of monads as a convolution! And thank you for the like :)
Regarding 2.: The general idea is that when you parse data from the outside world (like stdin) you don’t just validate if the input is correct but you also transfer the data into your type safe data model. That’s how the compile time guarantees also hold at runtime. You would normally use some kind of parser for this, like
aeson
for JSON orReadP
for general text processing.Got it. I’m guessing since Haskell makes it easy to define types or typeclasses, there shouldn’t really be a situation where an external type could not map to a Haskell type, or if there is, there’s a catch-all bytes type acting as a container of bytes?
IIRC I encountered some weird issues with various binary encodings where I found it difficult to map to any data type in Python without being forced to apply an encoding (and then having to strip out that encoding during export). For example, reading PostgreSQL
bytea
into a binary column in another database using a custom process, or capturing and rendering Java log4j stdin within a parent Python process. If Haskell was flexible enough to design parsers for arbitrary bytestreams from various protocols that would be a major selling point to data engineers.Sure, you can leave parts of your data in a more general data structure. Of course you can’t interact with these parts as easily, but if you just want to pass them through that’s ok. E.g. in aeson there is the
Value
type which is just a sum type over all possible JSON elements. This is still perfectly safe, because if you want to work with these values you always have to consider every possible case of the sum type. And of course there is stuff likeByteArray
.I think that syntax sugar for the bind operation is extremely important. Also, with static types tou Just can not not handle an effect; monads are not only a useful abstraction for composability but also for keeping track of effects.
I used to think that too, but I find myself wanting syntax sugar for applicative functor almost just as often.
Do you mean something like
ApplicativeDo
in Haskell?Yes, or idiom brackets!
I hate how people pride themselves with “X isnt as hard” and then go on about it.
The problem with the whole not-understanding-monads is because their applicability is extremely general.
On top of that tons of people randomly started praising using them (???) ~5 years ago.
I like the explanation given by experts in function programming the best. Monads are defined by their laws and nothing more. From these laws you can apply monads to a ton of problems. A lot of the time, you don’t even need to think you are using “monadic code”. It’s such a general thing people are using it all the time. I think that’s why it’s pointless as hell to a lot of people, and they “just dont get it”. It’s like being super confused about why people are losing their minds over strawberries are red. They are just red because that’s how they naturally occur for some very natural reasons. Well same for monads really. They just naturally occur.
Eventually though you will get the person who is interested in this generality. And usually these people “get it” straight away. Because they’ve seen the pattern many times over. And then they learn why all red objects in the universe are red.
I want to finish with: nothing is difficult when you understand it. Understanding is the difficult part in a lot of cases.
Yeah but the real issue is whether you support free-range monads or if you’re ok with industrial-bred ones
Nice. XD
Can someone clarify me modan/exception difference?
So in modan we’ll get Nothing at top level. And if we catch - Exception. Only Exception also contains stack trace which is more useful, then just “nothing”.
As I right?