So, one way to understand a monad is that it's essentially a container type with "map" and "flatten" operations. Let's say your monad is type M<T>. The "map" operation allows you to take a function T->U and a container M<T>, and transform each element, giving you a container M<U>.
"Flatten" takes a container of type M<M<T>> and returns a container of type M<T>. So a List<List<Int>> becomes a List<Int>.
Now comes the trick: combine "map" and "flatten" to get "flatMap". So if you have a M<T> and a function T->M<U>, you use "map" to get an M<M<U>> and "flatten" to get an M<U>.
So why is this useful? Well, it lets you run computations which return all their values wrapped in weird "container" types. For example, if "M" is "Promise", then you can take a Promise<T> and an async function T->Promise<U>, and use flatMap to get a Promise<U>.
M could also be "Result", which gets you Rust-style error handling, or "Optional", which allows you to represent computations that might fail at each step (like in languages that support things like "value?.a?.b?.c"), or a list (which gets you a language where each function returns many different possible results, so basically Python list comprehensions), or a whole bunch of other things.
So: Monads are basically any kind of weird container that supports "flatMap", and they allow you to support a whole family of things that look like "Promise<T>" and async functions, all using the same framework.
Should you need to know this in most production code? Probably not! But if you're trying to implement something fancy that "works a bit like promises, or a bit like Python comprehensions, or maybe a bit like Rust error handling", then "weird containers with flatMap" is a very powerful starting point.
(Actual monads technically need a bit more than just flatMap, including the ability to turn a basic T into a Promise<T>, and a bunch of consistency rules.)
> and they allow you to support a whole family of things that look like "Promise<T>" and async functions, all using the same framework.
You've highlighted here the part that would actually explain the purpose of a monad, but not explained it. You don't need a monad abstraction to have things with monadic properties, and indeed often reality isn't quite perfectly shaped to theory, so forcing your object to fit the abstraction can be costly. One very obvious cost is that you no longer get descriptive names of what the monadic bind does; you have to infer it from what you know of the type.
The one thing a monad abstraction definitely gives you is the ability to write code that's generic over all monads. This is weird because this almost never happens outside of strongly functional languages.
If you'll forgive linking to a decade-old Reddit post, I've talked about this before.
> You don't need a monad abstraction to have things with monadic properties, and indeed often reality isn't quite perfectly shaped to theory, so forcing your object to fit the abstraction can be costly.
Please note that I was trying to explain what a monad is, to somebody who wanted to understand. I also suggested that people writing typical code shouldn't actually need to know this in order to do their jobs:
> Should you need to know this in most production code? Probably not! But if you're trying to implement something fancy that "works a bit like promises, or a bit like Python comprehensions, or maybe a bit like Rust error handling", then "weird containers with flatMap" is a very powerful starting point.
Monads are an incredibly stripped-down mathematical structure ("container-like things that support flatMap"). And as such, people who design certain types of programming languages or libraries may benefit from being aware of monads. At least for languages with closures. Surprisingly few languages can actually support monads as a first-class abstraction in the language, because to make first-class monads nice you need a certain kind of type system. Which often isn't worth it.
Where I probably differ from your opinion is that I think implementing "almost monads" like JavaScript promises is very often a mistake. The few places where JavaScript promises break the monad laws are almost all nasty edge cases and obscure traps for the unwary. Similarly, if you implement list comprehensions that break the monad laws, most likely you just get awful list comprehensions.
There are exceptions. Rust has a lot a "almost monads", but this is mostly because Rust function types are a mess thanks to the zoo of Fn, FnOnce and FnMut. Rust would be a simpler and easier-to-learn language if Future<...> actually followed the monad laws. But in this case, it sadly wasn't possible, and I would argue that Rust is worse for having so many "almost monads."
This may all make more sense if you knew my tastes in programming languages, which is "languages where all the parts fit together cleanly with no surprising edge cases that prevent 'obvious' things from working." One way you can accomplish this is to have some kind of simple mathematical structure underlying your language, and to avoid adding dozens of features that almost follow clean rules. C++ never took this approach, and so C++ library designers need to be aware of all sorts of interactions between weird corner cases.
So another way of summarizing my argument is "If you have list comprehensions that somehow don't follow the monad laws, then you're going to confuse users and permanently add technical debt to your language. Make sure it's actually worth it."
The Rust example is illustrative, and indeed I was thinking about it when I wrote my post. One valid angle is to say that languages should hide the reality of the machine from the user any time it would get in the way of the pure semantic description. Another angle though is to say that actually these concerns are important, and if the result is that the idealised abstractions aren't sufficient to capture them, then it's correct to put away the abstraction. It's not like ‘simple and easy to learn’ is a function of this elegance either; Haskell is hard to learn and Go is easy, so clearly there's something else to it.
You also mention list comprehensions forming a monad. I think this is also a good illustration of the difference. In Haskell the structure of a type tells you the dependency tree of a computation, so it's not a problem that Monad maps the type to itself. Your list monad is just a bunch of thunks pointing to each other either way. In imperative languages, types describe what has been reified and how it's organised in memory. In these, a list monad is an actively bad abstraction; you almost always want to distinguish the stateful computational pipeline (eg. an iterator) from the source and target storage (eg. an array). Neither of those are monadic for good reason.
"Flatten" takes a container of type M<M<T>> and returns a container of type M<T>. So a List<List<Int>> becomes a List<Int>.
Now comes the trick: combine "map" and "flatten" to get "flatMap". So if you have a M<T> and a function T->M<U>, you use "map" to get an M<M<U>> and "flatten" to get an M<U>.
So why is this useful? Well, it lets you run computations which return all their values wrapped in weird "container" types. For example, if "M" is "Promise", then you can take a Promise<T> and an async function T->Promise<U>, and use flatMap to get a Promise<U>.
M could also be "Result", which gets you Rust-style error handling, or "Optional", which allows you to represent computations that might fail at each step (like in languages that support things like "value?.a?.b?.c"), or a list (which gets you a language where each function returns many different possible results, so basically Python list comprehensions), or a whole bunch of other things.
So: Monads are basically any kind of weird container that supports "flatMap", and they allow you to support a whole family of things that look like "Promise<T>" and async functions, all using the same framework.
Should you need to know this in most production code? Probably not! But if you're trying to implement something fancy that "works a bit like promises, or a bit like Python comprehensions, or maybe a bit like Rust error handling", then "weird containers with flatMap" is a very powerful starting point.
(Actual monads technically need a bit more than just flatMap, including the ability to turn a basic T into a Promise<T>, and a bunch of consistency rules.)