Functors and Monads for People Who Have Read Too Many “Tutorials”
jerf.org
jerf.org
I’ve used functors and monads to solve tons of issues and have literally no clue about the theory behind them.
You need to give the reader some real world example. A concept dangles in brain without connects to other things won't live too long. You brain is likely to `optimize it out` because it is unused.
That's completely fine. Haskell is not a single community. There are two big camps that often interact with each other, the academic and the industrial. Keep in mind the origin of Haskell is academic and it's original purpose is to test and implement ideas from Programming Language Theory. That is still there and will continue. GHC optimizes for letting people experiment with language extensions. Industrial interest didn't start to grow until somewhere around 2008-2012. It is a small community and will likely remain so.
I am a big Haskell user myself. There are many theoretical things I don't understand or need to touch, but I appreciate their contributions to the language and the community. I do agree that Haskell has lots of room to improve on tutorials for non-academics. Maybe I will get inspired to write somethings.
It's completely fine if you don't want to use it. However, you may find Haskell users' enthusiasm for the language insufferable, hehe. I do hope anyone who chooses to interact with the Haskell community finds us welcoming.
> ...too often act like ivory towers...
I 100% agree with this. The Haskell community as a whole and various tutorials etc., aren't optimised for a new comer.
For instance, you don't see a "stand up a web app in Haskell in 10 minutes" tutorials. Historically it may have worked in Haskell's favour to slowly induct learners, but in this age where languages are fighting for mind space I don't agree that's a good approach anymore.
Simon Peyton Jones (SPJ), an earliest and a famous champion of Haskell coined the phrase "avoid success at all cost" in a certain context which may have had some role in making Haskell seem Ivory Towerish.
A language, like any living being, needs to adopt to survive and thrive. It's unfortunate to see Haskell remain a niche language even after close to 3 decades.
> and that simply is not how my brain is wired.
This, however, I disagree with. I mean in a general sense, not specific to how your brain is wired. It all depends on the first couple of language that one learns and how they are learnt and taught. 10-15 years ago people would find it hard to grok Python's functional concepts. But as it began to be taught as the first language in universities and those graduates join the working population you see how it's super natural for them to grok Python.
That must be the reason why Edsger W Dijksta once stated [0]:
> "It is practically impossible to teach good programming to students that have had a prior exposure to BASIC: as potential programmers they are mentally mutilated beyond hope of regeneration."
... and ...
> "The use of COBOL cripples the mind; its teaching should, therefore, be regarded as a criminal offense."
... and ..
> "APL is a mistake, carried through to perfection. It is the language of the future for the programming techniques of the past: it creates a new generation of coding bums."
I must be one of those mentally mutilated programmers with no hope of regeneration :)
---
[0]: https://www.cs.scranton.edu/~mccloske/dijkstra_quotes.html
I include myself. It has been a definite struggle to onboard functional concepts and solve problems in a functional style. Just doesn’t come naturally.
Why do you feel the need to denigrate people who are experienced in imperative and/or object-oriented paradigms?
If there is any paradigm that holds its own value, why not praise the value it adds instead of resorting to nothing but ad hominems?
I mean, if something was so unequivocally better then wouldn't this perhaps resulted in a massive adoption rate, and name-calling wouldn't pop up so frequently? But no, the popular thing to do is to just denigrate those who haven't jumped into that particular bandwagon.
That's not what we see happen in life. Just look at how dearly we hold on to coal plants. My country's energy is 90+% coal-based and there is an insurmountable opposition to atom, and great suspicion against solar (government recently started financially deterring against mounting solar panels).
What a non-sequitur. Adopting a programming paradigm in the code that we write on a daily basis has rigorously zero to do with the infrastructure cost of switching energy sources.
This non sequitur about coal plants is even more absurd and ridiculous once we factor out the fact that the world is already moving away from coal to renewable energy sources, which is quite costly and resistant to change and subjected to an awful lot of special interest groups, and yet during that timeframe people still chose not to bother with functional programming fundamentalisms.
If an idea is good then it holds its own against alternatives. Even in their pet projects people tend to not even bother with pure FP frameworks. While hundreds of millions of euros are being spent on wind farms, people like you and me don't even bother spending a few minutes to get a hello world going with Haskell, even though it's effortless and trivial. Why'd you think that's happenin?
Yeah, over a long time it will certainly happen that a good idea will manifest itself. But that process can take a long time, centuries even.
Let's not forget that there are commercial interests for keeping certain languages down and pushing languages up. There are huge companies like Google, Facebook, ... pushing for languages (and frameworks).
It's often easier to stick to something existing because change requires effort. And I think that's why coal was mentioned - it's easier to stick to it than to switch to something else, so it will take time but eventually it will happen.
He really isn't, and the blatant attempt to poison the well is a clear indicator of the lacking arguments.
I repeat: people don't even bother spending a few minutes trying to get a pure FP hello world going. Zero barriers to entry, zero challenge, zero resistance.
But in spite of the lack of any obstacle or challenge, pure FP fails to present a value proposition that justifies even a five minute effort from pretty much anyone.
> Yeah, over a long time it will certainly happen that a good idea will manifest itself. But that process can take a long time, centuries even.
This baseless assertion holds no water in software development. It does not take a multi-generation epifany to convince someone to use pure FP. All it takes is a single person willing to invest it's time.
But still, those who do invest their time with ivory tower tests don't see value that justifies pursuing any effort, and thus don't bother pushing it anywhere beyond a cursory test.
Why is that?
> It's often easier to stick to something existing because change requires effort.
This assertion holds no water at all, as pure FP frameworks exist for decades and still people do not bother with them. Why is that? And how can you hold such a cognitive dissonance of claiming something is so great and yet it does not exist in practice because no one ever bothered to take advantage of such greatness?
Also, are we supposed to pretend that even Microsoft offers pure FP languages that integrate with their .NET stack and still no one bothers with it at all, even when they can even contam it's use to very small and self contained modules?
The truth of the matter is that pure FP fails to gather at attention beyond navel-gazing ivory tower types because it quite blatantly fails to present a value proposition. That's the core of the issue. I mean, languages like Rust are increasing popularity like wildfire in spite of it's radically different and restrictive take on resource management because it presents a clear and inequivocal value proposition. But pure FP frameworks, in spite of having a head start of decades and a foothold on dark corners of academia, fails to convince the public that it's in their best interests to even peek in that general direction. Why is that? Do we really need to resort to absurd conspiracy theories to get an answer to that question?
First, FP required/requires more resources on average. With time progressing and better hardware coming, this point becomes less important.
The strengths of FP, such as easier concurrency, are becoming more relevant as well.
However, even universities take time to "catch" up. And developers often stick to the style they first learned (or a similar style at least). In addition, they already created and invested into an ecosystem.
But look at all the new programing languages that are getting created. They include more and more FP features, starting with the more easy ones that have a direct impact on productivity (such as lambdas) but it's getting more and more. Even languages such has Java start to slowly move into this direction.
Or do you want to deny that?
Also, I have no idea what you mean by "FP frameworks".
there is no govt forcing/de-incentivising functional programming afaik...
Carpenters don't learn "hammer-oriented" building, or "saw-oriented", "screwdriver-oriented", "chisel-oriented", or "glue-oriented". They learn to use hammers, saws, screwdrivers, chisels, and glue, and use them all at different times, sometimes one more than others. Machinists don't learn "lathe-oriented", "drill-oriented", "mold-oriented", or "welding-oriented" fabrication. They do all or any of them strictly according to what they are making, and what they are making it out of.
"This-" and "that-oriented" is a entirely a sales method used to package up coursework and seminars. It is as utterly stupid to design a whole language around one of them as it would be for a carpenter to try to run a screwdrivering business. A language is useful exactly to the degree that it enables building whatever you might be called upon to build.
That is not to say any generally useful language is equally good for any purpose. Wood is better for some products, metal for others. A metal violin would be weird, a wooden gun action would be stupid. But violins often have metal bits and guns often have wood stocks.
Elitism and gatekeeping do precisely nothing to help people solve problems with code.
They are indeed, aren't they?
I mean, they straight up sound like bullying those who happen to not think alike or share the same opinion.
Those who are in the right are able to form rational arguments, raise concerns about specific problems, and offer solutions. But no, not in this case. The only reason for anyone to not be a diehard supporters of the author's point of view is that they have mutilated minds and are coding bums.
Don't we take things a bit too serious these days? I've started programming with BASIC and I find the quotes kinda funny - I don't take it too serious and I am not sure if Dijkstra was being dead serious here either. For example: would anyone really think Dijkstra was in favour of locking COBOL teachers behind bars?
I agree that this self righteous fake outrage targeted at those who point out bullying and abuse gets a bit too tiring, and adds nothing to the discussion.
This is a very good example. Calling out the lack of rational arguments or any reasoning regarding a technical subject, and resorting to abuse to fill the void with ad hominem and bullying, is expected to be addressed with explanations on the technical merits of the original proposals.
But no, here we are wasting time with chatter that boils down to "why aren't you ok with being subjected to abuse? Don't we need to roll over and shut up when someone throws ad hominems?"
I guess we don't? But it seems that some people whine when they can't take what they are dishing out.
I agree 99% here. The 1% is where you want to shock someone to break through. Not sure if that was Dijkstra's intention, but I assume it was. He was a teacher as well, writing books, giving lectures. I think he was genuinely concerned.
Unfortunately, elitism and gatekeeping "solve problems with code" indirectly, by keeping inadequate people away from developer roles where they would make problems worse with negative-value contributions. Software mistakes are common and expensive: there are very strong incentives to predict and prevent them by following processes and by putting someone competent in charge.
It is of course possible for elitism and gatekeeping to select the wrong sort of people (for example, not recognizing terrible people outside a manager's area of expertise), but even intolerable toxic attitudes (e.g. hiring graduates from certain universities) can effectively keep away dangerous people.
If there are people who are inadequate at writing code and architecting software, then they should:
- not manage to pass their university classes and not get a degree or a programming qualification
- not manage to pass their internship by generating some value and proving that they're capable of learning and self-improvement
- not manage to get the necessary certificates for a particular technology, as a vague proof of basic competency
- not manage to solve the take home tasks that they're given by a company that's about to hire them or pass technical interviews
- not manage to pass onboarding for some months and therefore should be fired
- not manage to deal with their duties as a developer and therefore should be fired
Of course, depending on different cultures, these things could change (e.g. bootcamp instead of university education, personal projects instead of certificates), but none of those involve calling someone: "...mentally mutilated beyond hope of regeneration," just because they used BASIC, PHP, Python, or any other easy language that let them solve easy problems without getting too deep into the internals of programming languages and how computers work.Furthermore, the difference is in attitude - if a person doesn't pass one of the above, they can probably just upskill themselves and spend more time refining their craft, reading books, working on projects etc., whereas dismissing them entirely is likely to demotivate them and so they'll never achieve anything. It might also be selecting for the wrong types of people - those who simply brush off criticism like that and don't care, as opposed to those who are more sensitive, something that should hardly matter in regards to developing code.
There has to be a better and more constructive way of criticizing people and even dismissing them: for example, saying "Hey, your programming knowledge seems okay, but you should work on your system design skills. Try applying for a job in a year again," vs simply ghosting them.
> Software mistakes are common and expensive: there are very strong incentives to predict and prevent them by following processes and by putting someone competent in charge.
Lastly, this feels like the job of:
- the compiler, for errors and warnings
- the IDE and language server, for general suggestions
- the linter, for code style rules
- static analysis tools like SonarQube, for additional code checks
- testing frameworks (including code coverage gates), for unit, integration and end-to-end tests
- manual code reviewers, after everything above has been resolved by the code author
- QA specialists, after all of the previous checks have been successfully passed
As for having competent leaders, sernior developers, architects, security specialists etc., i wholly agree! That's not to say that the culture of software development needs to be a parody of anti-social behaviour."Evaluate the expression on the right of the = sign and make this the value of the variable on the left."
I'd like to argue on how reasonably simple is simple enough. If I need a carpenter to build a stairs I'm my house, I would expect a good carpenter to know and use all the tools appropriate, no matter how complex. Of course, most things are doable with just a hatchet just fine, but I d expect a professional to choose a hatchet because it's better, not because they never bothered to learn proper tools.
So whenever a language puts "easy to learn" as their main feature, I see it as a guide "how to build stairs with just a hatchet".
There's now the IHP framework that allows you to build haskell web apps in like 10 minutes. If you're curious, check it out: https://www.youtube.com/watch?v=UbDtS_mUMpI It's been called "Haskell on Rails", and for good reason.
"avoid success at all costs" is usually meant to be parsed as "avoid [success at all costs]" i.e. don't compromise the language just to make it more popular.
This phrase is often misunderstood, it's "avoid, success at all costs", not "avoid success, at all costs". In other words, don't optimise for mass market adoption at the expense of everything else. Languages that have arguably done so, have ended up as extremely complex and ridden with corner cases.
It's easy to follow a rule too far and end up way overcorrecting the original problem. What I like is that both meanings balance each other :)
Some popular mass-market friendly deliverables don't cost very much at all, avoid them and you're definitely avoiding success at all costs, but with both meanings at once this time.
As I understand it, the core Haskell folks don't especially care about Haskell's (admittedly limited) real-world usage. Or, if you prefer: real-world usage is a side-effect, not a value.
Yes, there is a lot of research being done on Haskell, but a lot of the research is centered on areas that more or less directly contribute to typical real-world usage. Consider the recent introduction of linear types for example: They're both exciting from a type theoretic perspective, but could also improve some real-world scenarios (even though at this point linear types are still quite new, so there's not a lot of examples out there).
> This, however, I disagree with.
I think it might well be correct, actually. I think that it might be the difference between abstract and concrete thinkers on that standard personality test. Abstract thinkers will find that Haskell makes more sense; concrete thinkers will find that C makes more sense.
For balance, here's another common opinion about tutorials: http://dev.stephendiehl.com/hask/#eightfold-path-to-monad-sa...
> [...]
> Read the monad definitions.
> Use monads in real code.
> Don’t write monad-analogy tutorials.
In the end, the two opinions about learning monads coexist, and people have done fine following either way.
First of all, I completely understand where you're coming from.
But the fact that Haskell embraces computer science, unlike most other programming language communities, is what attracts me to it the most. I can geek out about the mathematics that leads to simpler, safer software with like-minded people. I am always learning from Haskellers more than from any other programmers.
But it does take a lot of learning and unlearning to become a fluent functional programmer. This isn't because functional programming is more complicated than other paradigms (au contraire), it's because schools and universities have been mostly teaching OOP for the past 2-3 decades—unfortunately. As a result, most programmers have such a big gap in their knowledge that it can feel too daunting to dive in.
We could definitely do a better job teaching the theory and explaining how it's useful. That would be better for everyone: better for curious minds who want to expand their programming skills, and better for me because I'd have more company to discuss it with.
In particular, most algorithms research and papers are expressed in imperative pseudo-code, as that is, in fact, much easier for humans to intuitively reason about than complex category theoretical concepts.
We're not talking about any complex category theoretical concepts here. Functors, for example, are one of the first ideas one would learn in a category theory course. Likely even in the first lecture.
Even the most advanced functional programs use only the most basic categorical constructs.
And when you say "imperative pseudo-code", you've already conjured an implicit monad involving state, I/O, etc. Functional programming just teaches us that it's not the only one—and there are simpler ones that may be more appropriate for the given situation.
No, I have not. The fact that you can recreate state and I/O using monads does not mean that anyone using state is implictitly using a monad - monads have very specific properties that imperative code often doesn't have.
It's only Haskell's laziness that makes it require monads for i/o, by the way. Other pure functional languages don't use them. For example, in Idris, IO and state are not monads, they are effects - tracked at the type system level, but without all of the commodity of monads. For example, you can have a single do-block in Idris that does IO and state operations without needing any monad transformers or lifting.
I still think they don't often come up in algorithms research, though.
Why are you so fixated on algorithms research in your attempts to dismiss the usefulness of functors and monads?
Basically, I am only trying to dismiss the idea that Haskell is somehow closer to CS or more scientifically correct than other programming languages; instead, it is as related to CS as other languages are, just choosing to focus on a different area than many others.
I also want to dismiss the idea that CS == FP, that imperative style reasoning is just some second-tier style not used in rigorous circles - much of the forefront of CS is indeed using imperative style constructs more than FP ones.
You don't know what you're talking about. In the context of programming language theory, monads were first used by Eugenio Moggi precisely for the purpose of specifying the semantics of imperative programming languages. Only later did Wadler realize that they could be useful as user-defined constructs within a programming language.
The "very specific properties" are actually just the identity and associativity laws of a certain monoid. These are trivially satisfied by imperative-style code including the "algorithms research and papers are expressed in imperative pseudo-code" you mentioned: you can write an imperative program that does nothing when sequenced with other programs (identity), and A; B; C does not require explicit grouping (associativity).
> It's only Haskell's laziness that makes it require monads for i/o, by the way. Other pure functional languages don't use them. For example, in Idris, IO and state are not monads, they are effects - tracked at the type system level, but without all of the commodity of monads. For example, you can have a single do-block in Idris that does IO and state operations without needing any monad transformers or lifting.
This is an incoherent train of thought. Haskell's laziness does not require the explicit use of monads, nor are monads only useful in the context of laziness. Haskell could have used Idris's IO system instead—algebraic effects work just fine in lazy languages. But also it is not true that Idris programs do not use monads just because you don't see the word "monad" in Idris programs. Effects give rise to a monad, mathematically; namely, the free monad for the algebraic theory. Read the seminal work by Plotkin and Pretnar, for example.
Just no.
The reason Haskell is so complicated to become fluent in has nothing to do with the inherent complexity of functional programming. The Haskell community is just in love with complexe abstraction for the sake of abstraction, extremely terse and hard to understand syntax (see for example the obsession with introducing convoluted operators) and generally favour hard to understand style like point free where pipes are a lot easier to follow. The Haskell community is full of people who are here to geek out rather than produce software. Haskell is what happen when you let one upmanship leads your language design.
Meanwhile, you can learn SML, F# and Ocaml in a couple days, gradually enjoy the functionality they have to offer and benefit for a nice community. I really don't understand why anyone would choose Haskell.
I can't count the number of times I have read, here on HN, that people only choose imperative programming or OOP because they're ignorant or stupid. Because if they actually understood FP then of course they would choose it, so the only reason they don't is because they haven't learned or can't learn.
No they don't. Every Haskeller I know (myself included) acknowledges that sometimes point free style is better, and sometimes it isn't. None of them would unilaterally declare it better, and most generally avoid it except in very simple situations.
> The Haskell community is full of people who are here to geek out rather than produce software.
The Haskell community is full of people who are here to geek out about the best ways to produce software. This means they embrace tools like mathematics, and are generally extra thoughtful about structuring programs in principled ways. It's a wonderful community of people who care about the details of their craft and treat it like a skill to be honed over time. I have a lot of respect for that.
If I could rant for a moment: I'm really sick of spending many years of my life investing in myself and my ability to produce software with the best tools humanity has to offer, only to have these people in Hacker News tell me I'm just doing mental masturbation when it seems like they don't even understand what they're criticizing.
\x -> h $ g $ f x
At a certain point you realise that in many cases seeing the `x` is not just useless but needless visual noise. The following is identical and more readable once you've internalised composition:
h . g . f
And this extends to a cute trick in which, if you need to explicitly provide that data to begin with, you can do this:
h . g . f $ x
I'm now working with Haskell having previously come from PHP and JS/TS. Composition is more readable, it's just harder to get started with because it's different to what you're used to.
As for the "geek out" comment, of course, I enjoy programming for the intellectual sake of it. I'm not product-driven. Don't make the mistake of thinking everyone's the same as you, nor that of thinking there aren't benefits to being so intellectually-motivated. I don't know if it's intentional but your comment comes across as rather self-righteous.
With a ‘theoretical’ description, like a BNF grammar or a Unix man page listing every possible option the program accepts, this is immediately obvious. Haskell’s focus on ‘theory’ follows the same philosophy.
In any case, all knowledge needs to be elevated to some level of theory because a correct theory guarantees the repetition of correct application.
This post helps on some of these points, like explaining that e.g. functors are not always containers.
I’d love to see someone try taking it further: just hashing out lots and lots of substantially different examples, showing how they are similar to each other, and progressing into higher level transformations, before finally showing the definition of a functor, and how it maps to each of those cases. Forget monads. Can we just start with functors and see how far we get with that?
I think this is how a lot of people learn math and programming anyway. Build theory out of intuition.
Post-secondary math classes were actually amazing to cultivate the habit of persisting through long periods of feeling like a complete idiot. But I have to wonder what the same content would have been like had it been example-driven.
It reminds me of learning a language through exposure to endless examples and internalizing rules without being explicitly taught them, versus deliberately memorizing the grammar rules of a foreign language and then applying the rules to specific examples.
Often when presented with a definition it's easy to get fixed with examples that are too specific and then to conflate the definition with something a bit stronger.
Ugh. So much this.
I've had so much difficulty grokking mathematical concepts and procedures in my life. In high school, I fell behind so hard.
Algebra I was doable, and while learning Geometry I felt like I already knew everything about the subject. I never studied and the admins concluded that I must be cheating, given my performance in other courses.
I concluded that in order to learn these things, I had to have "something to do with it". So to speak, some relatable examples to which I could apply the concepts in order to internalize the rules. This strikes me as the source of the never-ending choruses that are echoing still through so many study halls. ringing out, "WHEN ARE WE GOING TO NEED THIS???"
If feels so good when the concepts click, it's like learning you had a superpower. Suddenly, the baroque, mysterious walls of lines instantly resolve into such crisp focus, it truly is an amazing epiphany, Ah-Ha!
I feel like I could have really enjoyed the discipline, but I never achieved this feat in my educational career. I had problems that dramatically redirected the course of my life. This is probably why computers appealed to me so much, because of the direct cause-and-effect nature of interfacing with them. The Hacker Manifesto comes to mind, but I digress.
I have Narcolepsy Type II and probably ADHD (as you may infer from my comment history) and let me tell you...
How I actually learned the math was by examples and easy exercises, not understanding proofs.
class Functor f where
flip fmap :: f a -> (a -> b) -> f b
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
Sorry, that's not doing much for me! I am now going to look for a tutorial on Monads that's written in, say, Python, so at least I don't have to learn an abstract new language alongside the abstract new concept.I agree with you entirely that the Haskell syntax is probably one of the biggest issues with monads being approachable.
I don't find lisp syntax confusing either.
I've tried to read up on some Haskell basics, but the main issue is I don't have any real incentive to use Haskell, I have other hobbies that are more tempting, so I forget pretty quick.
The Java-ish version of the Functor interface for instance was quite readable to me, ugly or not.
It probably won’t be very helpful. The definition of a monad is 90% in the types.
People use Haskell to talk about monads for two reasons:
1. Its type system is expressive enough to represent them (parametric polymorphism and higher-kinded types)
2. It’s one of a very small number of languages that doesn’t let users wiggle their way around needing some kind of principled effect system, so you really can’t get away without monads. Only in such an environment can the social pressure to take shortcuts be overcome, forcing people to structure their code carefully enough that the rich concept of monads isn’t reduced to a few occasionally used and probably incorrectly implemented corner cases like optional values and maybe futures.
There seems to be a disproportionate amount of tutorials about monads. Not being able to provide sufficient amount of examples in other languages makes monads seem quite less useful. Maybe next article to write is “why should you learn about monads”.
The definition of a monad, as it relates to programming, emerges when you realize that an Option type, a Result/Either type, a List type, and a bunch of other non-container types all have the equivalent of a `flat_map` function. Write out a few mathematical rules for how a well-behaved implementation should compose and you have a Monad and its rules.
This is why another approach to learning monads that isn't focused on the theory introduces it as a design pattern instead. I find this to be great for intuition but horrible for understanding personally, having used both approaches to teach people about Monads IRL.
If your type system can't express them then you lose most of the advantages and they're pretty pointless.
Why that type signature? You only have two options: that, or m (m a) -> m a. The former is more popular for ergonomic reasons mostly. It doesn’t really matter.
it's basically continuation-passing-style (`a -> m b` is the "continuation"), you might as well ask "why can you represent so many control-flow things using CPS?". idk why, but you can!
from another angle, you could compare Monad with the less powerful Applicative. a formulation[0] that's easier to parse than the usual one[1] is:
class Functor f => Applicative f where
unit :: f ()
pair :: f a -> f b -> f (a, b)
if you're familiar with JS Promises, a rough analogy would be >>= (Monad) Promise.then
pair (Applicative) ≈ Promise.all
unit (Applicative) =
return (Monad) = Promise.resolve
ignoring parallelism, you can implement Promise.all using Promise.then, but not the other way around.---
[0] Called "Monoidal" here: https://stackoverflow.com/q/45267953 [1] https://en.m.wikibooks.org/wiki/Haskell/Applicative_functors...
At first glance monads seem less useful in languages with builtin mutable state, and freely allowing side effects. However, if you’re writing somewhat more high-level domain specific code there are plenty of concepts that can be modeled using functors and monads very elegantly.
Parser combinators, distributed data fetching, reactive data streams, composable tree traversals, etc
I use them all the time in for TypeScript.
I would really like to understand what they are, but so far have not been able to find any explanation which tries to explain them in terms of english, math or some commonly known programming language.
I can provide you a simple example of a function with monadic interface, you will understand it any language but that will not actually help you to conceptualize the idea of monads and influence your thinking.
You will just think that's simple, whats the fuss about it and miss the pattern altogether.
Imagine asking the user to provide a number, in most languages you will get a `string | null` value and you wish to get a real number from it by parsing the string if the user entered it.
In this case assuming you have a function `parseNo` that goes from `string -> int | null` you can create a special function which takes a `string | null` value along with the `parseNo` function and gives you back `int | null`. S
Turns out that function can be defined for many different data types and it has special useful properties.
In this case it allows us to compose functions neatly, without having to do a whole bunch of null checks.
But in other scenarios it can do a lot more.
So something can behave similarly to a monad, but the type systems don't stop you self-sabotaging your code or misunderstanding good composition.
Best way to learn is to learn Haskell deliberately. Better than Go, the language forces you to compose better code, and even learn the values of FP.
If one is uninterested in learning CS or expanding one's toolbelt, it'll take some years until industry catches up.
This is about 90% of the way there (and TFA actually contains very similarly phrased description, right before the monad definition you complained about).
The problem is that English does not have the right words to describe the abstract idea behind "source", "way of" and "operation" precisely and rigorously.
Regarding "any common programming language": In a way, this is like trying to explain a concept like, say, "the adjoint operator" [0] to someone only aware of integers. Or trying to explain the idea of the Liskov Substitution Principle [1] to someone who just learned assembly.
In each case above, the language used to describe the concept is multiple layers of abstraction too low to succinctly ping down the concept in question. Haskell brings (or rather is) a very rich language suited specifically for accurately describing the elements involved. That's why 1) the concept of a Monad is visible/cleanly expressible in the first place, and 2) people use it to explain the concept, even when (like this blog post) not aiming at a Haskell (or Haskell learner) audience.
Disclaimer: I'm just a lowly OOP programmer, not written a line of Lisp or Haskell in my life. Watching from the sidelines for now.
[0]: https://en.wikipedia.org/wiki/Hermitian_adjoint
[1]: https://en.wikipedia.org/wiki/Liskov_substitution_principle
Did you mean to write "Say you have a source of A, and a way of turning an A into a B. An operation that uses these two inputs to produce a source of B is a monad"?
What exactly do you consider a "source"? And the way of turning an A into a B I would call a function. And combining the "source of A" with a function that converts A's into B's, a concationation or composition of functions, or a way of currying.
And yes, I am fully aware, that you sometimes need to learn the vocabulary to have a minimum discussion. But considering I have a degree in math and CS, have learned at least 10 programming languages, especially Lisp with all of its contexts, I am highly suspicious about something that cannot expressed in plain language. But thanks, your explanation comes closest I have seen so far.
No. That would be the Functor concept explained in TFA. A Monad is specifically what I wrote.
> And combining the "source of A" with a function that converts A's into B's, a concationation or composition of functions, or a way of currying.
Yes, pretty much. See the article (where one of the main points is specifically that the functor concept is not a highly complex thing. Just a name for a kind of thing - an abstraction).
> What exactly do you consider a "source"?
That's exactly the point where the "genericness" of the concept (consider: it originates from category theory) precludes discussing it in more specificity. It could be a Maybe<A> (std::optional in C++), it could be a List<A>, it could be a PotentialFutureUserInput<A> (aka IO in Haskell), it could be a (C#) Enumerable... Anything that "wraps" (in any sense) another type. A functor allows you to apply a function that transforms the inner type into another without leaving the wrapper. A monad allows you to apply a function to the inner type, transforming it into a different now wrapped type, with the monad implementation taking care of "flattening" the wrappers. I will avoid the attempt to come up with an example, seeing as the article also criticizes the abundance of bad monad examples.
Here, I've written a C++20 concepts version of Monad. Well, I hope I did. But even if correct, it's just not something C++ can express well, much less use well: Since functions are not first-class citizens, you can't spell "a function from A to M<B>" in a way the compiler can deduce. That's why you need the extra template argument F and corresponding constraint FAMB.
You can provide a function with the type signature `a -> b`. With this we run the function underneath `m` and map `m a -> m b`.
Concretely this might be with for example lists, where given a function `Int -> String` I can map `[Int] -> [String]`. What the functor abstraction gives you is a consistent, lawful way to define this phenomenon for virtually every type you'd ever want to "map". Likewise monads for "flat mapping".
This explanation was 90% accurate. What's missing is that not every functor or monad is a "container", for example Haskell's `IO`, which rather represents an action of sorts to impurely get a value. But the intuition will build all the same if you play with it a bit. It's all in the type signatures.
> tries to explain them in terms of english, math
There are a million English-language blog posts, and it’s a mathematical concept. Just look up “monads are monoids in the category of endofunctors” if you want more of that…
"If you don't need IO sequencing, or purity because your language doesn't care about purity, or the list implementation's nondeterminism because that's a parlor trick, or STM because your ecosystem doesn't use it... there's not much point to worrying about it. Monad implementations in languages other than Haskell fail to take off because there generally isn't even a single thing that is improved by working with it through a monad interface, let alone a family of such things."
"[...]It's definitely an interesting idea, and if you are a curious programmer, it's worth learning about this particular interface and what it can do. But it's not something you generally directly take back to your language"Nonetheless, these are all monads, and learning specific monads helps in lots of other languages. Optionals are nice in Java. Arrays in Lisps. Error handling in Scala. But the "universality" of monads escapes these languages. I would kill for better monadic error handling in Go, but conversely I would have little use for monadic I/O or futures.
The critical mental step is seeing the commonality in all of these, which is really only possible in languages at that level of type sophistication. And necessary in Haskell where the particular combination of purity, laziness, and HKTs means monads are the only way decent way to do most of these. Once you see that commonality you can develop tools - patterns of mental reasoning about code and idioms for writing code - you can also use when dealing with the specific monads or nearly-monads in other languages. But you can't show that property in examples in a single other languages, and it's difficult to show how some deep structure is shared in examples spanning five languages.
Nevertheless language ecosystems outside Haskell, scala and F# don't recognise this as a general programming pattern, although given the current direction towards FP in the JavaScript and rust communities I think it's a matter of time.
To put that a bit differently: Haskell is a strait-jacket. In the name of purity and laziness, it takes away a lot of things that other languages give you. Monads are the hoop that is left, which you have to jump through in order to be able to do what you can do in a more straightforward way in other languages.
I'm saying this in a negative way precisely because you said it in a positive way.
But which is it really? Is it a positive or a negative? Depends on who you are and what you're doing. Depends on how much your program is improved by not having the wiggle room, and how much of the time you're doing what is easier to do in other languages.
("Easier" might not be the right word. Do notation isn't that bad of a notation for expressing sequencing in. It's a bit cumbersome, but it's not terrible - it's not like building the sequence monads by hand. It's still producing them under the hood, though. Whether that's a problem depends on how badly you need speed, how much pressure the garbage collector is under, how much of the program needs to be sequential, and so on.)
for example, if "f" is "list of" and a is "integer" and b is "string", then flip fmap would take as input a list of integer, and a function from integer to string, and produce a list of string.
If you've done some topology: [or, maybe a is a particular topological space and b is another topological space, and f is "the fundamental group of", then given an element of the fundamental group of a, and a function from the topological space a to the topological space b, it will give you a corresponding element of the fundamental group of b.]
a and b are both types of things, f takes in a type, and outputs another type. so, a and f a are both types (usually different types). the thing is that f doesn't just let you make a type from another type, it also lets you translate a function from the type a to the type b, to a function from the type f a to the type f b.
It's easy if you first learn about Haskell notation and type classes, which pretty much every online tutorial covers. What you quoted isn't arcane, you're just unfamiliar with it (just like my granma would find C-like syntax illegible: she just wouldn't know anything about it).
# milk_cows :: Person -> Tired Person
def milk_cows(person):
...
person["tired"] = True
return person
# feed_pigs :: Person -> Tired Person
def feed_pigs(person):
...
person["tired"] = True
return person
# birthday :: Person -> Person
def birthday(person):
person["age"] += 1
return person
# willy :: Person
willy = {"age": 32, "tired": False}
# fmap and >>=
print(milk_cows(birthday(feed_pigs(willy))))
In Haskell there would be a 'Tired a' type with a Monad instance to express the tiredness property and a Functor instance to apply functions to it with no impact on or knowledge of tiredness, e.g. aging.Haskell folks love arrows and one letter variable names.
I don't know why, I guess it's the algebra background of most Haskell folks.
When I wrote some things in Haskell, I tried to use full words for function names and variable names, but it felt I am the only one. People just love to name stuff `a` `b` `m` `f` `k`
interface Functor {
<T, U> Self<U> fmap(Self<T>, Function<T, U>);
}
which is just torture. What would it look like in your ideal syntax? interface Functor {
<Source, Target> Self<Target> fmap(Self<Source>, Function<Source, Target>);
}
Same code, but you get an additional hint of what's going on.A definition like:
bind f (x:xs) = f(x)
would fit this type signature, but would very much not be a monad.What makes a monad a monad is that it is a functor (so a special parametrized type of sorts) with
- a function to wrap a single value (wrap :: a -> M a)
- a function that can flatten nested versions (flatten :: M M a -> M a)
- such that wrapping twice and flattening is the same as wrapping once
- such that when flattening a triply nested structure it doesn't matter which level you flatten first.
And of course it also needs to satisfy the functor axioms, which are also frequently overlooked, though they're not too difficult they just mean that 'fmap' needs to play nice with identity functions and function composition.
And Haskell in particular suffers quite a bit from the fact that currying is a first class concept, while in category theory this is reserved for a rather special class of categories. This means that using functors in combination with functions of multiple arguments suddenly requires you to think about how to evaluate a tree of functions on a tree of values, where in most other languages you'd end up with a tree of tuples at worst. Granted you can do more if you solve those problems, but it requires you to think about concepts that even category theorists consider pretty 'out there'.
Would you agree that introducing particular interface is gonna show interface definition in question?
What should follow (just like in Java) is few examples of actual implementations and how they fit into that interface.
However, I can't help but feel that the author and I have some common flaws.
The first being verbosity. I often notice the same thing in my own writing: I will see a well-written paragraph and think "this is good stuff" and then wonder "but what purpose does it serve here?" Stuff like dedicating 5 paragraphs to discussing whether Functor should be a noun (I get the point, but that's a lot of words to make it), or a paragraph with caveats about how Haskell does name resolution.
The second being that we both seem to have fallen victim to the monad tutorial fallacy (https://byorgey.wordpress.com/2009/01/12/abstraction-intuiti...). But I'm not 100% sure about this one. I don't think it's wrong to call monads interfaces in the same way that it's wrong to call them burritos. However, our framework of thinking does seem to align with what that author refers to as fallacious.
Still was a good enough read that I felt the need to comment partway through :)
The paragraph starting with "I am aware of the notorious effect" directly addresses this, do you disagree with it?
Also calling it an interface isn't an analogy.
I suppose my answer should be "yes," but let me try to explain my convoluted reasoning:
Understanding monads simply in terms of interfaces is what I think is the silver bullet to comprehending them. It's what helped me "get" them. So they are my "burrito."
> Also calling it an interface isn't an analogy.
I agree, I apologize if I came across this way.
(This isn’t an entirely new kind of concept in programming, either; see “patterns”.)
which to me is an interesting debate given Haskell's emphasis on strong interfaces strictly enforced.
We seem to be converging on a world where we agree that declaring and enforcing types is very important, yet also that it's the structure of the type, not its name, that is most critical. At some point, with sufficiently advanced type inference, do you end up in a world that looks like a lot like fully dynamic languages, where the actual declaration of the type is purely optional?
I strongly agree with valenterry's comment[1] here, that post gives is an approximate definition of functors, even omitting the functor law. In particular, it is explaining what an endofunctor is (in the context of FP, you mostly deal with one category anyway). Furthermore, even in Haskell, there is no such thing as the category of Haskell types[0] (because of things like divergence and partiality).
For the curious, I've recommended Program Design By Calculation[2] to a number of people to show how category theory can be used as a powerful tool to reason about programs in an FP context while not compromising on the mathematics (the target audience is undergrad CS majors).
EDIT: In particular, I have a bit of issue with this sentence after stating the signature of fmap, since it does not reference lawfulness.
> This is the full definition. There is no additional hidden machinery that Haskell is somehow automatically invoking when you "use a Functor".
[0] http://math.andrej.com/2016/08/06/hask-is-not-a-category/
[0] https://leanprover.github.io/programming_in_lean/#01_Introdu...
[1] https://github.com/leanprover-community/mathlib/blob/master/...
Divergence and partiality can easily be modeled by the category of complete partial orders and Scott-continuous maps. This is what you learn in, e.g., a course on domain theory and/or denotational semantics.
Andrej Bauer is mainly arguing that Hask isn't a category because no one has formally specified it. For example, we would need to come up with a policy on when two arrows are considered equal, and it's not clear what that notion of equality should be.
A quote from Bauer's post is particularly relevant:
> [I am arguing against] the fact that some people find it acceptable to defend broken mathematics on the grounds that it is useful. Non-broken mathematics is also useful, as well as correct. Good engineers do not rationalize broken math by saying “life is tough”.
> A function is a relation for which each value from the set the first components of the ordered pairs is associated with exactly one value from the set of second components of the ordered pair.
Cool, I have no idea what the fuck this means and how it is useful to me.
Oh wait, I use them every day.
> Oh wait, I use them every day.
Do you really? Do the `functions` you use even fit the above definition?
I bet you can think of infinite number of functions (bool -> bool). The above definition admits only 4 such functions.
I don't think there is a shortcut... these are highly abstract concepts.
Abstraction is about exposing the most essential parts of something... Category Theory is perhaps the mathematics of abstract composition, exposing the most essential parts of how to combine (in the abstract). Monad and Functor arise from CT, and thus, are very abstract concepts, built from the axioms of an already abstract subject matter. This is why it's hard to "simplify" them any further. The formal definition of a Monad is itself the simplest way to expose it. The fastest way to grapple with it is through the axioms of CT, precisely because CT is arriving at this concept by abstraction; by only thinking about the most essential parts of it.
That is probably not a satisfying explanation (to tell someone you need to learn axioms and laws before understanding this concept), but the alternative is to simplify and/or abstract something that is already as simple and as abstract as can be, albeit with esoteric names, and thus inevitably complicating it and shrouding the concept instead. i think the reason so many Monad tutorials aren't as satisfying is because they attempt to explain both a mathematical, abstract concept and also how it applies to programming, at the same time (without explicit separation). The latter is practical, the former is very abstract, and the bridge between them is very complex and usually left out, leaving the reader scratching their heads how and why you go from one to the other.
It's worth mentioning Bartosz Milewski here (links below), as he has bridged this gap for many, but as mentioned, there is no shortcut.
Upon hearing this, the islanders think that functions must be some magical panacea, and so you backtrack on that and try to talk about functions as being like animals, they take input and produce output (good luck with function composition).
The situation is like that with monads. They are far removed from the world of programming (the fact that there were connections at all to effects was remarkable!), but as such, you have to learn the ambient theory in which monads reside to really grok it.
If the author is reading this, I might make a suggestion: connect the hash map example to the function example.
Mathematically, a function is typically defined as literally a (potentially infinitely-long) map of unique keys (domain) to values (codomain). In programming, the two are not identical, especially in mutable languages, but the hash map example would be like memoizing function composition for a set of inputs specified as keys in the hash map.
> However, interfaces are adjectives that describe a data structure, which is why in many languages they are often named with -able suffixes (Iterable, Serializable, etc.) Programmers are better off thinking of it as Functable or Fmapable or something like that.
Fmapable makes much more sense to me. Naming is hard.
Worse... their usage in programming languages is very unlike how homomorphisms are typically used in their usual context, so appealing to the math seems like its just confusing learners without payoff.
To give an analogy... it feels like trying to explain the chain rule by explaining pullbacks between tangent spaces. Not technically wrong, but now they're confused and asking questions whose answers wont help them.
IME, category theory was much more valuable in differential topology in helping solidify why the structures and their compositional properties make sense. You get to work with several, honest-to-god categories and functors between them, especially when tangent bundles come into the mix.
Now go to programming, there's really only one category at play, and I'm not sure how worthwhile it is to apply a (more contrived) version of CT that only has endofunctors and being cartesian closed.
For programming... I feel like they're basically trying to do logic in a roundabout way. I suspect that (finite) model theory might be more useful for such applications if they really want theory.
This puzzles me. It turns out it's very useful in the context of programming in practice (i.e. Haskell programmers use it and find it useful), so what do you mean? I don't know CT so I wouldn't know how well it maps (pun intended) with the Math concept, but regardless, whatever abstraction is there in Haskell that programmers call "functors" is tremendously useful.
For example, when using the Van Laarhoven free monad[0] implementation, I'll quantify over all monads. While, there is a simple data type definition for free monads[1], it suffers from a well-known issue that the bind operation has quadratic complexity. The Van Laarhoven free monad circumvents this problem (at least in the common case where you are not trying to "execute" the intermediate monadic constructions). There are also continuation-based approaches for free monad implementations as well.
The benefits of free monad are the typical ones described in "Data types à la carte" where you can, for example, instantiate your free monad in the IO monad for production, but use a pure monad instance for (parts of) your test harness.
(Generic implementations of free monads and their operations is another example of quantifying over all Functors, but I'm assuming for the sake of argument that you'd prefer to repeat that boilerplate over and over again for all your specific free monad instances.)
[0] http://r6.ca/blog/20140210T181244Z.html
[1] https://hackage.haskell.org/package/free-5.1.7/docs/Control-...
[2] http://www.cs.ru.nl/~W.Swierstra/Publications/DataTypesALaCa...
I think it's a small convenience to have the programming language automatically derive `map` instances for you, but I think you're right for pretty much the same reason group theory's huge but monoid theory barely exists: the `Functor` typeclass is too simple to do anything interesting with it.
Example: I do mostly embedded prgramming, in Rust. My focus is on building working devices. I use abstractions and build APIs that are useful for solving specific problems I have now, or anticipate in the future. Eg an init function for an ADC that does the register writes described in the reference manual, so I can write `let adc = Adc::new()` instead of a series of reg writes.
It's common in embedded rust programming to make heavy use of generics, traits etc, and other things that leverage the type system to make it so that as many mistakes are caught at compile time as possible.
This sounds great in theory, but if a given abstraction makes type signatures a mess, doesn't allow me to configure a pin correctly due to an edge case, and provides no advantage if I QC I'm using the right pin, I won't use it. I see these Haskell examples as using abstractions by default, without questioning whether it's appropriate for a given use.
Because, a lot of the convenience and ergonomics that we can derive using these abstractions comes from being able to rely on the laws. Without them, they are just not really useful.
And when looking at the examples, I can totally see that someone reads
> That's all Monad is. It is an implementation of some "method" that conforms to that interface specification.
and then makes "Set" or "HashMap" implement the interface unlawful and bad things happen when they are least expected.
In the same way that we expect addition of numbers to obey the commutative law and associative law. (Except, of course, that floats don't...)
Implementing "unlawful monads" would be equal to giving someone a float and telling them "you can use this like a rational number from math". And in fact it might work for the longest time - until it doesn't (think rounding-errors and similar) and then the confusion will be big.
With monads it's the same, but potentially even worse, because it's rather easy to test float addition behavior, but for certain monads that can be much harder.
Just as yo want to break addition commutativity so you can approximate real numbers, there are times when you want something to behave like a monad, but not follow its laws.
Ie it's not just the interface that matters, but rules about the implementation.
IIRC, Haskell relies on two out of the three, but I don't remember which two. But if you write a "monad" that doesn't satisfy them (even though it has the right function signatures), then you're going to get bogus results.
x+y+z is associative.
x-y-z is non-associative.
Functor, monoid and monad laws allow for undefined evaluation order, lazy evaluation, parallell execution and same results for same parameters. But only if the laws holds can such be guaranteed when using certain abstractions. Associativity being one obvious caveat that might break abstraction over collections, while using divide & conquer mechanics such as function currying.
It gives a lot of theoretical background, but if you read the article here and already know the Optional-type and then you can skip it and go directly to "A Real World Example".
I label this my opinion quite on purpose, because I see the alternative viewpoint. I think you'll be more successful taking about lawfulness once someone gets the mechanics, given how much that has been a stopper over the years!
However, Monad and Functor are well defined terms. If you write an article where you complain about confusing explanations and try to do better, then you must start with a disclaimer: "I'm not explaining Monads as they are defined in math. I'm explaining something that is quite similar to it to make it more practical and later explain the actual term" or so.
Otherwise you are just adding to the confusion that people experience.
> The laws are less useful and less relevant in strict languages, as is the case with many laws, which is why Haskell programmers are just about the only ones discussing them.
I'm not sure if I would agree with that. But even if I would, the laws are still really important, even in strict languages. I've implemented quite a few monad instances in Scala (which is a strict language) and I'm speaking from experience.
(in case they are unfamiliar with the notation f : A -> B , then this can be explained in each of the first few examples.)
I guess it might also be good to give some counterexamples, and point out how the counterexamples fail to be functors, and why they are also not nice.
> The problem is that a "do" block in the list monad chains together several of these flatMap calls. It is very easy for the transformation function to result in the computation growing exponentially as each one expands one item from before into one or more items. You have to have something else in the problem that prevents that from happening. You may see a lot of cute little algorithms like solving the n-queens problem, but in practice, this is not something you see used a lot in Haskell. The mathematical transform is cute, but hard to control because it's a bit too simple. In particular, this has no ability to "notice" that it is revisiting possibilities it visited before. That would take more machinery.
I think he's saying that it's doing a depth-first search without any caching. I'm wondering if Haskell has some other nice way of specifying depth-first searches that performs better?
This dialectic style adopted by the article for explanation is effective in exploring confused understandings. (Bit socratic) Not all writing needs to be technical writing like in a manual -- narrative explanations can be useful too.
Functor = map
Monad = flatMap
Did I understand the article correctly?
edit: this is an honest question. I way I read his explanation of Functor is that it's a function that implements map (list as input -> list as output).
His explanation of Monad said that its an input of List, where each item returns it's own List, then the function returns a flat List of all the results.
For all the functor tutorials, it really is nothing more than the interface and rules that define map. And Monad is essentially flatmap.
That's was so absurd about all of this.
Definitely not arbitrary but maybe not useful for many monads (??)
So in a sense yes it's "just" this function that could do anything following the signature.
But now you're in the same place you already were, looking at a simple type signature with massive hidden implications, and if you treat it like a list you'll miss those implications.
"flatmap generalized to a vast field of dissimilar types" is much more complicated than just "flatmap"
"It is a common misconception that "monad is the same as flatmap", which has even made its way into some "monad libraries" in non-Haskell languages. flatmap is not "monad". flatmap is the particular implementation of the monad interface on lists. If you understand flatmap, you understand the monad implementation for the list datatype... you do not understand "monads" in general."
Maybe you think the author is wrong, but your comment didn't read like you were addressing these points.If you can read and understand this comment (https://news.ycombinator.com/item?id=27639779) then you understand 80% of the practical use of Monad.
And the majority of people who understand that comment, could now implement the flatMap/join/>>= for the "Maybe a" Monad. And could probably implement ">>" for it as well.
And if you can implement "bind" for a randomly requested Monad, while you may not know what a Kleisli Category is, nor be able to define the monad laws and might still make a mistake implementing one, you have the essential concept of a Monad.
To be concrete, I'm talking about the kind with the semantics that if `Parser X` parses an `X`, and `Parser Y` parses a `Y`, `>>` between them parses an X then a Y. I propose that you must explain that in terms of flatMap in order to say that this is a correct understanding of monads.
flatMap() is a specific function that you already understand. It is the most common instance of a general fn pattern that is used for the specific data type Array.
The flatMap equivalent function when used on the data type Promise is called then().
The special function you are asking to understand ">>" is simply another instance of then(). This one still waits for the promise to complete - the only difference is that the lambda you pass to then() doesn't have access to the result of the previous promise.
If you're using >>/flatMap on a Parser datatype, instead of running the lambda after first promise has completed successfully, it "runs" it after the first parser has completed successfully.
flatMap and (all of its other data type specific names) are the essence of a Monad.
Well, the extra sauce on top of an Applicative Functor that makes a Monad; but the essence of an Applicative Functor is also part of the essence of a Monad, and the essence of a plain Functor is part of the essence of an Applicative.
flatMap doesn't let you implement the audit log example, or work on a binary tree, let alone try to implement something like IO.
What I am still unclear on is the difference between applicative and monad. Both, to my mind, have this concept of combining "wrapped" values. Currently my intuition for the two is exactly the same. So my intuition is probably wrong.
I would love a similar explanation to this post on applicative vs monad.
Functors let you chain together unary (non-branching, straight line) processing pipelines. If your processing pipeline works over any functor, then it can be applied to any structure that acts as a source of values, and it will leave that structure unchanged around the processed data.
An "applicative" is a functor that also lets you take two values, one structuring some `a`s and one structuring some `b`s, and cross them together to produce a result that structures some tuples `(a, b)`. If I have two lists, I can cross them together to get a list of all pairs of elements between the two lists (like a CROSS JOIN in SQL).
Applicatives let you merge two separate processing pipelines together, combining data from both sides together into a single stream. If you can combine two pipelines into one, you can combine any number of them; this is what makes them strictly more powerful than general functors, which cannot support multiple parallel data sources.
A "monad" is a functor that also lets you take a value with two layers of nested structure, `f (f a)`, and meld/flatten/unify those two layers into a single layer of the same kind (`f a`). If I have a list of lists, I can unwrap the inner list structures, lifting all of their elements to the top level. Or, more viscerally (for me), if I have a binary tree whose leaves contain binary trees, I can graft the inner tree at each leaf onto the outer tree, producing a deeper single tree.
Monads let you accumulate some inner structure produced by a stage of your pipeline into the outer structure your values came from. In turn, this reveals the values under the inner structure to downstream pipeline elements.
The community is very much focused on learning, by teaching the little one knows.
So far all I got from this is that functors are functions?
I prefer to just write down the type and stare at it and think about what sort of functions could have that type. Later you can look at the rules. And this mostly means one needs to have written a little (eg) Haskell so that the one can read simple types. The signature of fmap looks like:
fmap :: (a -> b) -> functor a -> functor b
It should easy to guess what sort of types and functions could satisfy that. The rule restricts the possibilities more: fmap (\x -> f (g x)) a == fmap f (fmap g a)I dont want to discourage you from learning math, but I think this isnt a good place spend too much thought at first.
I've heard a bunch of people say that category theory is almost like a 'bird's eye view of mathematics'. That many of its concepts unify many mathematical concepts under one umbrella, and reveal the similarities and connections between mathematical subdisciplines.
The main reason this interests me is that I'd like to learn math much differently than its taught.
To me, to learn the same concept under different subdisciplines with different names, is O(N) learning.
However, if someone put together a category theory book where they list a concept, and then they do this:
"Now, any mathematician can (easily) see that every major area of mathematics is a category.
- In Set Theory the arrows are functions
- in Topology they are continuous functions
- in Group Theory homomorphisms
- in Linear Algebra they are linear transformations
- in Differentiable Geometry they’re smooth maps, and so on…
But what’s important is that we shifted from focusing on the objects to focusing on the functions, the ways in which we transform the objects. This is basically the category-theoretical perspective: it’s the functions that matter."
(Taken from this blogpost: https://catsinthejungle.wordpress.com/2008/11/15/category-th... )
Do I understand any of that? No. But what attracts me to this format is it (MAYBE...) approximates O(1) learning in math, so long as it can also point out, for every concept in every field, how they differ/relate to every other concept.
So what this does is it helps teach me the 'unifiers' of mathematics, and helps me see connections between fields which can be useful for creativity. E.g. I realize I'm doing X algorithm here from linear algebra and that is O(N), whereas if I just shift fields, and use Y algorithm from Group theory I might make it O(log n) because it differs in this way or whatever it is.
Besides that I see it as a potentially efficient way of learning a lot of math.
While a shortcut to understanding sounds nice, I don't think you'll find it by focusing on abstract formalism.
In this case, seeing the connections across the disciplines isn't necessarily something I see or am pursuing because I'm searching for a shortcut (although I did mention O(1)), I just see that as a side benefit. What appeals to me more is developing the intuition you mention via the analogies. I feel like if the idea is restated in different ways (not necessarily just terms, but perhaps visualizations/re-conceptualizations) a more holistic/intuitive/fuzzy idea emerges rather than anything rigorous necessarily, but you know a lot more math than me so maybe I'm saying nonsense.
- a parametrized type, like for instance List: you can have lists of integers, floats, functions, or any other type
- a way of mapping (in the sense of List.map) between instances of the parametrized type, given regular functions. This mapping should behave nicely with respect to regular function composition.
If your parametrized type is F, then the mapping operation turns ("lifts") regular functions A -> B into functions (F A) -> (F B) in a reasonable way: "if you give me a function then I can transform any list using it".
Functors can be used to "decorate" values: you can model exceptions (some value or an error), promises (some value not yet computed), state (some value + side effects) and many other kinds of effects using functors.
So in general an effectful computation will have type A -> (F B) for some functor F modeling the effect. And now you have a problem: how to compose effectful computations in order to build larger programs out of smaller ones? You cannot simply compose A -> (F B) with B -> (F C) since the types don't match.
What you need is a way of transforming an effectful computation B -> (F C) into a lifted function (F B) -> (F C) so that you can compose A -> (F B) with (F B) -> (F C) in order to get A -> (F C).
That device should satisfy some properties to make effectful composition work the way you expect (in particular there should exist identity effectful computations), and is called a monad.
A monad is a functor + some extra operations (bind/return) satisfying properties that will make effectful computations (using this functor) composable.
The type of bind is generally written (F A) -> (A -> F B) -> (F B) but really it is the same as (A -> F B) -> (F A -> F B) above (takes an effectful computation, gives back a lifted function).
isn't this just applicative? What is needed to turn applicative into monad?
Here's my best understanding so far:
A functor is a function that takes in a list of things and like a regular function, outputs a list of modified things...
So how is this different from a function?
To give a simpler example, we say a function, f, is monotone when for x<y, f(x)<f(y); that is, it preserves order. Every monotone function is obviously a function, but functions like x^2 are not monotone.
Basically a functor is a function which preserves some other properties. I wont go into detail as many have before me, but thats the gist.
For those who like me wanted to see the edge case for the non-monotonic function, if you do:
-2 < -1
-2 is in fact less than -1.
However:
(-2)^2 < (-1)^2 ==becomes==> 4 < 1
And 4 is obviously not less than 1.
While monads as a general interface might not be in your favorite language, individual implementations of monads might very well be. JS Promise, C# Task and Rust Future are all asynchrony monads - their bind is .then / .ContinueWith / .and_then respectively (with some caveats; the first two overload their bind to also work as fmap).
In practice it's not too much of a problem, but it does mean that if you use some FP library, you cannot trivially wrap a promise to make it a monadic type that will work with the library's monad utilities.
There's a bunch of history in [1], particularly [2] and [3], if you care to read them.
[1]: https://github.com/promises-aplus/promises-spec/issues
[2]: https://github.com/promises-aplus/promises-spec/issues/75
[3]: https://github.com/promises-aplus/promises-spec/issues/94
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
Now, I am comfortable with upper case for type and lower case for instance.But m is an instance of a function, not a variable. But m, a, and b all look like variables. You are already hammering me with the brevity here.
Maybe put the functions in a serif font, and variables in a sans-serif? Or use different colors? Other languages seem to be more helpful with the visual cues.
Solving problems in SQL is a blast, so it seems like Haskell should be an easy lift, but the brevity of the syntax has sent me packing for years.
Monad is a typeclass, not a type. m is a variable, whose value is a type constructor.
> But m is an instance of a function, not a variable.
On the type level, m is a variable representing a type constructor; a concrete value it might have is List. It is an instance of a typeclass, not a function.
> But m, a, and b all look like variables.
m, a, and b are all type-level variables; the value of m is a type constructor (which is essentially a type-level function), while the values of a and b are types.
If it were a physical building, Haskell would be brutalist.
Let's try to apply that knowledge, and use my tribe's creed that "similar things should look similar, and different things should look different":
-- α, β types
-- m type constructor
(>>=) :: m α -> (α -> m β) -> m β
-- a, b types
-- 𝘮 type constructor
(>>=) :: 𝘮 a -> (a -> 𝘮 b) -> 𝘮 b
I think that's much better. GP's complaint was correct, writing different concepts with the same notation is confusing.It's hard to find best intro material. The above could scare away someone else, even Haskellers.
Maybe intro on how to read examples, explanations etc. could aide?
I think it’s better to at least try to make a name that transmits partial meaning or a general gist of the idea.
If we didn’t use formal terms for math concepts, mathematicians would waste all their time arguing about the colloquial definitions of the colloquial words they tried to map onto abstract concepts. It’s better to have a clean break and force people to learn the specific formal meaning rather than try to limp along on a busted half-intuition.
similarly, monads bear a strong resemblance to monoids - in fact, it's the same relationship as functions and functors and it's quite precise. these intuitions only become available to you with the math, though, and programmers have a strange aversion to mathematics for people who do it day in and day out.
This sentence from it sums up many of my conversations about FP
It probably also doesn't help that we have a function literally called >>=, which is hard to pronounce. It's pronounced "bind", which is only slightly more helpful in that that still fails to evoke any useful imagery
One question on functor: Does the thing returned have to be "the same shape" as the original? That is, if I have a list, and I fmap on it, do I always get a list back? Or am I only guaranteed to get some kind of functor back? If I am guaranteed to get a list back, am I guaranteed that it has the same number of elements?
To use your initial language, is it fair to say that a functor takes a source of a, and a function that converts a to b, and returns a source of b of the same "shape" as the original source of a?
If it's true, that makes the function example clearer to me. Of course it has to return a function, because the shape of the input was "function".
(And, did the article answer this and I just missed it?)
fmap :: (a -> b) -> f a -> f b
The `a` becomes a `b` (which could be a different type but not necessarily), but the `f` is the same `f`.I've recently been interested in learning more math to apply to programming. Does anyone here on HN know where I can find quality education online (free) I can use through the rest of the summer? I only got to geometry as my highest maths in high school.
- "return x" produces a I/O operation that does nothing and produces the result x.
- "x <$> y" or "fmap x y" produces a I/O operation that does the same thing as y, but the result value is the function x applied to the result of y.
- "x >>= y" produce a I/O operation that when performed, performs x, and then passes the result to the function y, which returns the I/O operation to be performed next.
- "join x" takes the I/O operation x which has the result which is itself a I/O operation; the new one first does x and then the I/O operation which is the result of x.
A I/O operation might have different results when performed multiple times, even if it is the same I/O operation.
Also, I can say that generators in JavaScript seem to form two different monads actually (as far as I can tell), as described by the following table:
yield x yield*x
return x return yield*x
Also, functors and monads are more than the interface, but also the mathematical properties they have (such as "fmap id = id" etc).There is nothing inherently interesting about functors and monads. Haskell, as a research project, discovered that you could isolate IO in a lazy language inside a type using a monadic construction to chain them which allows you to track side effects. That was interesting, the rest less so.
Now the Haskell community somewhat fetishized these abstractions for reasons which are their own. They must be apparent to the community at large. They are not to me.
Sounds like LINQ in C#. I wonder if there's any correlation.
I personally don't think Haskell stand at the right level of abstraction but that's a personal opinion.
a <- method b
c <- method a
is equivalent to
a = await method(b)
c = await method(a)
Again. Use javascript to learn Monad in real world is a better way to learn.
It's like having someone run hello world in java and declaring they know what 'static' means.
Also, JS promises are not really monads. They lack denotational equality, so they can’t be said to obey the monad laws.
(I know, I know - your environment is a special case).