Grokking monads really requires the adoption of the mathematical mindset of finding commonalities in things that at a first glance appear completely different. Tell an average OO programmer that lists, exceptions, dependency injection, and asynchronous execution all share a common structure, and they will probably give you a blank stare.
Of course, just the fact that abstracting over those things is possible doesn’t mean it is useful. In a pure FP language it might be necessary, but why should I bother with weird mathy things in my imperative language that has side effects and global state? You really have to start by explaining why composability is such a nice thing to have, and that gives the motivation for various FP patterns that are, fundamentally, all about composability.
But then you start running into the nasty issue that when you need to compose multiple monads, you find out that monad transformers don't always commute!
There are good reasons, like encapsulation, etc. But the real reason is, it just dosn't feel right and there is this urge to fix it.
Once you have this feeling of obviousness, how do you transmit it to the next person? Many things can be explained, but in math it was figured out long ago that at any given time there are many things we don't know how to explain, and aren't sure we ever will be able to explain in a way that transmits understanding faster than experience can instill it. The best thing you can do for someone is give them the definitions and some problems to work on. We can't rule out that someone may eventually come up with a brilliant explanation that provides a shortcut to understanding, but we know from experience that some things persistently defy our efforts to explain them. If hundreds of people's earnest attempts to explain something have failed, then perhaps teachers should keep trying, but learners should not waste their time with these experiments; they should skip the explanations and seek active engagement with the idea through problem solving.
That's how I feel about monads. I can't absolutely rule out the possibility that someday an effective way to explain them will be found, but I think we can agree at this point that there is ample evidence that people who want to understand monads should not waste their time waiting for the right analogy to be blogged and posted on HN. They should just start programming, and soon enough they too will feel like a great explanation is on the tip of their tongue.
I have had this experience, that I've moved much faster learning to use math tools when a friend much more advanced than me suggested that I can use a tool even if I don't know how it works. It's like if I stopped building a house because I want to see if I can build a hammer myself !
(Well, this is also my experience programming, implementing the low level stuff myself is a much more attractive exercise than actually assembling off-the-shelf parts into something people would actually use!)
What I've observed is that a lot of learning programming is about becoming comfortable with thinking of more and more concepts as first-class entities. Turning larger pieces of code, procedure, and patterns into object/values you can pass around, hold, create, etc.
1. small-scale one I see a lot is that many programmers don't realize boolean expressions produce values. Instead, they think they are syntax that can only be used inside control flow statements. It is a mental leap to realize that you can go from:
if (thing == otherThing) { doStuff(); }
moreStuff();
if (thing == otherThing) { doLastStuff(); }
To: var sameThings = thing == otherThing;
if (sameThings) { doStuff(); }
moreStuff();
if (sameThings) { doLastStuff(); }
2. In some sense, recursion is about thinking of the procedure itself as an operation you can use even while being in the middle of defining the procedure itself. The mental trick is "Assume you do already have this procedure, how could use use while defining it?"3. Closures are another big one where you take a piece of code and turn it into a value that you can delay executing, pass to other procedures, etc.
- They're explained badly.
- Once they're explained well, many (including me) think it's a bad idea (not the monad, the motivation behind its use in pure-FP).
You start with a pure functional formalism, because you like to be stateless. Then you realize that avoiding statefulness is impossible in computing. So you try to shoehorn state into your stateless state of affairs (no pun), while at the same time refusing to admit that you're not stateless anymore.
The larger issue: some folks appear to think that imperativity is a subset of declarativity.
What that really means is that, they're saying that, computing is a proper subset of math.
And by that, what they're really saying, is that actions are a subset of words.
In other words, if you write something on a piece of paper that describes some action in the real world, (roughly speaking) that action happens or is supposed to happen automatically.
That's now how the world works. That's not how computers work. And I'm sorry to say that's not how programming works.
Computing is not a subset of mathematics. And Mathematics is not a subset of Computing. Same goes with Physics (Physics is not a subset of Mathematics, despite there being way more math used in Physics than CS).
Physics, Computing, and Mathematics are the holy trinity of the reality that we live in. You have to give each of the three the respect they deserve, and only try to make connections between the three, and NOT try to make any of them a subset of the other.
if i understand you correctly, are you saying that mathematics in the classical sense is not “computing” (von neumann machines?), just as math is not physics, but math is used to model physics
then the proper way to think about the problem is: can there be a math that models actual computing (does that include physics?) as it is today, and is that what we already have in our programming languages today?
and finally, maybe your main point is, with purely functional languages, there is a math invented (lambda calculus?) to describe a stateless ideal that is shoehorned into a reality that it doesn’t describe?
apologies for the random thoughts, just trying to grok everything ^_^
We like to model stuff in computing and stuff in physics, using mathematics. What we're really doing is creating maps (math models) of the territory (computing, and physics). We notice there is some part of the stuff not fully mapped, so we make our map more complex and more elaborate. We keep making our map more and more complex not realizing that not everything about the territory can be mapped, because territory has an existence independent from the map. No matter how complex we make the map, it cannot explain away the territory itself.
What pure-FP advocates are claiming is that that pure-FP is all that's needed for computing. Then they notice (or someone points out) that there is stuff like computing state that is not pure. The pure-FP people go 'no problem, we have monads'. But now they've made pure-FP more complex, and it's not pure anymore. And it now depends on external execution to happen to have state updated. What they appear to be doing is trying to explain away imperative and stateful aspects of computing, which are there as an inherent part of the computer, existing and running in the real world.
You might be interested in a past discussion here [2].
much food for thought
When trying to map (or bind :) a monad to OOP/imperative programming, it strikes me as more straightforward to think that, e.g., the IO monad is an object that encapsulates the result of a network operation, together with some utility functions for dealing with the result and not having to deal with the unwrapping of the result. Kind of like Futures or Promises.
(Now, here the real FPers will say that the comparison is flawed because of certain FP desiderata like referential transparency, but that's beyond the extent to which I've internalized monads.)
But for many people, they will be used to IO returning nothing, or error values they can ignore. So the case has to be made as to why you need a monad there at all.
(I've just had the slightly disturbing realisation that C++ streams are also monads .. the documentation never uses this term.)
You might like [1].
It's part of Bartosz Milewski's extensive discussion of C++[2], but I'm unsure how that mass can be accessibly approached... maybe by starting around 2011. I very fuzzily recall liking his "Re-discovering Monads in C+" (2014)[3], but I didn't quickly find video. Apparently he gave a "Monads for C++" talk in 2017[4].
[1] https://bartoszmilewski.com/2014/04/21/getting-lazy-with-c/ [2] https://bartoszmilewski.com/category/c/ [3] https://www.slideshare.net/sermp/rediscovering-monads [4] https://www.youtube.com/watch?v=vkcxgagQ4bM
I think this also. You need to pick a language that fits your problem area, but also one that fits your brain.
Encapsulation? No one gets direct access to the state. Instead, there are methods or functions for dealing with the state indirectly, crafted to protect those outside.
Answer: wrap the entire external universe and all its messy state up into an object, then pass that down a chain of functions which can return "universe, but with some bytes written to the network" (IO monad)
Sounds like "Outside the Asylum" from the Hitchhiker's Guide to the Galaxy universe. Basically, someone decided the world had gone mad, so he constructed an inside-out house to be an asylum for it.
http://outside-the-asylum.net/
"A monad is a type that wraps an object of another type. There is no direct way to get that ‘inside’ object. Instead you ask the monad to act on it for you." How is a Monad anything different than a particular kind of object wrapper?
2) "A monad is a type that wraps an object of another type. There is no direct way to get that ‘inside’ object. Instead you ask the monad to act on it for you." Your objection to this quote is right, because the quote is wrong.
It _is_ a particular type of wrapper object. That's where the whole "in the category of endofunctors" comes in. An endofunctor being a functor from something to itself.
You have IEnumberable<SomeObject> and that lets you do SelectMany to flatmap some internal nested set of objects down to IEnumerable<SomeOtherObject>.
The shape of what you get back doesn't change. You get back an IEnumerable of something. That has a specific contract on which you can do specific operations regardless of the object it is wrapping.
The other piece of the puzzle is that it is monoidal. A monoid is just a collection of objects + a way of combining them + a 'no-op'. This is usually worded something like "a set of objects with an associative binary operator and an identity".
The classic definition "a monad is just a monoid in the category of endofunctors" is worth picking apart piece by piece. But it's also utterly useless because you have to spend quite awhile picking it apart and then looking at concretions of every one of the individual abstractions to understand what the hell each part individually looks like and then put it back together in your mind.
That definition is classically used as a joke because it's so terse you have no hope of understanding it without a lot of study, yet at the same time it's so precise it's all the information you need!
An object wrapper has to have the object somewhere in memory, you just can't touch it. With a monad, the object might not even exist yet (example: Promise) or there might be more than one (example: Collections).
How so? Why is it such a big distinction? Why isn't that just encapsulation? "Handling mutable state in a 'stateless' way" is basically just Smalltalk style Object Oriented programming. (As opposed to Java or C++, which has some differences.)
dirty languages:
input -> function_a -> output/input -> function_b -> output
^ ^
| |
side_effect_a side_effect_b
| |
v v
lovecraftian_primordial_soup_of_global_state
pure languages:
input -> function_a -> output/input -> function_b -> output
^ ^
| |
side_effect_a side_effect_b ------>
|
+-------------------------------------------->
If C++ were pure, the type-signatures would look like (output, side_effect_a) function_a(input);
(output, side_effect_b) function_b(input);
The drawback is that the type-signature of function_b(function_a()) becomes complex. Now, function_b needs to accept and pass-on the upstream side-effects. To compose function_a and function_b, we need to convert the type-signature of function_b to (output, side_effect_b, side_effect_a) function_b(input, side_effect_a);
Fortunately, ">>=" converts function_b under the hood. Which allows us to write function_a() >>= function_b() >>= function_c >>= function_d
and pretend that each ">>=" is just a ";" without wrestling with compound inputs and compound returns.Note that this informal statement doesn't necessarily mean you have to be encapsulating data. A behavior, a contract, an assertion, compositions of all of these etc can also be encapsulated.
Fun ideas (not necessarily true but fun to think about):
* Monads kinda convert nesting to concatenations.
* A monad is the leakiest of the abstractions. You are always a single level of indirection away from the deepest entity encapsulated within.
* What's common among brackets, curly braces and paranthesises(?) is them being monadic if you remove them from their final construction while keeping the commas or semicolons.
Very important note: You should have already stopped reading by now if you got angry to due these false analogies.
We can have arbitrarily nested monads:
monadicObj.bind((T value) => monad.wrap(monad.wrap(value)));
Remember, `bind` only unwraps one layer. Without it unwrapping one layer, programs would continue accumulating large stacks of monads in monads.I would also point out that it only collapses abstractions of the same kind; Maybe's bind only unwraps one layer of Maybe's. If you have a Promise<Maybe<Foo>> where Foo contains potentially more monads as instance-variables, those don't all get collapsed.
I like the 'converting nesting to concatenating' observation.
Sometimes we do need parentheses though, because most languages are not associative 5 - 2 - 1 is not the same as 5 - (2 - 1). Basically minus does not form a monoid, so the parens matter.
Some other references helped me along the way:
* http://www.lihaoyi.com/post/WhatsFunctionalProgrammingAllAbo...
So, the Adapter Pattern, but for types?
Like Lisp, there seems to be about 10% of the programmer population who think "ah this is obviously the clearest way to do it" and the remaining 90% who go "huh?", and the 10% are really bad at explaining it in a way the others can grasp.
The two monad explainers that really resonated with me were:
- How do you deal with state in a language that would prefer to be stateless? Answer: wrap the entire external universe and all its messy state up into an object, then pass that down a chain of functions which can return "universe, but with some bytes written to the network" (IO monad)
- If you have a set of objects with the same mutually pluggable connectors on both ends, you can daisy-chain them in any order like extension cables or toy train tracks.
(It's a joke, but people need to recognise why "A monad is just a monoid in the category of endofunctors" is a bad explanation 99% of the time and understand how to produce better explanations)