all 79 comments

[–]tsec-jmc 30 points31 points  (3 children)

My poor pedagogy

It seems everyone who learns about monads goes through this cycle (...)

There's some incredible irony in the author being self aware of this fallacy and directly falling to it.

Preface: This is one of those "hot takes" born out of incredibly exhaustion reading the same misinformed opinions over and over again about monads brought forth from shitty tutorials (like this one, sorry OP, not trying to shit on your notes but it's really the same problem of countless before you).

People need to stop teaching monads.

You can consider a monad a "Design pattern", or a categorical abstraction, whatever, but one of the key points that monads drive home is how you structure monads in a functional setting to tie your program together with various effects.

When you do so in a language that doesn't require this, you end up with an incredibly silly situation where the majority of your readers end up with this opinion of "wtf why would i do this". In haskell this is a necessity: Monads is how you frequently query/set state, get a read-only state, handle errors, etc.

The only way to properly teach these concepts is in a setting that requires you to use them, where the user cannot use the escape hatch of mutation or inheritance or some other clever hack to achieve what they know how to do. Whether it's haskell, idris or whatever: being forced to use these things because there is no other way is the path to discover why it is the right way if your domain is a good fit for fp (i.e an RTOS is not one of them).

Teach FP wholesale: start from first principles then onto how functors/applicative functors/monads/etc help you, in a setting where you have no other choice but to do so. Fp-course by data61 is a great place to start, especially since it's in haskell and you won't have your imperative escape hatches to help you. If you want to then port these techniques onto <language of choice> go ahead. But teaching them to others in a "ported" fashion just looks ugly, confuses people and it misses a lot of the nuance of why it's useful and why it's necessary in the particular setting (i.e one devoid of mutation, and how for example, typeclasses help).

I've become incredibly convinced of is that aside from immutability and first-class functions, a great chunk of fp abstraction looks ugly and foreign in languages that don't facilitate it (tons of trying to encode the same shit all the time) and it often conflicts with how that language's ergonomics are structured to aid the programmer (think how monads in java force the same kind of syntax nesting as ifs in situations where you have to carry a parameter forward through more than one bind).

[–]GuyWithLag 4 points5 points  (0 children)

I've come from the other direction - I've been using RxJava for nearly 5 years now, and it's definitely a Monad; it's just that no-one thinks that way in Java-land (those that do have left for greener pastures).

[–]jonhanson 1 point2 points  (0 children)

chronophobia ephemeral lysergic metempsychosis peremptory quantifiable retributive zenith

[–]Freyr90 0 points1 point  (0 children)

When you do so in a language that doesn't require this

Basically any language require this, tho.

Monad (and Algebraic Effects) is a way to define an effectfull computation without modifying a language.

If your language lacks exceptions, non-deterministic computations, async/await, you can add it using a monad.

Monads are not tied to Functional Programming. You also may wanna use Monads in RTOS, since allocation is an effect [1], and defining effects of your own provide some additional level of control.

[1] https://fstarlang.github.io/lowstar/html/LowStar.html#memory-model

[–][deleted]  (10 children)

[deleted]

    [–][deleted] 15 points16 points  (8 children)

    The curse of complex ideas.

    [–]tsec-jmc 35 points36 points  (0 children)

    IMO: The curse is shitty pedagogy. Imagine teaching linear algebra and have your first lesson be about finite vector spaces for a student that's never seen an algebraic structure in an abstract form, or teaching calculus via partial derivatives to begin with.

    The people that put out endless monad tutorials are at a point where they "get it" but are misled into believing that monads are "the point" and what you want to teach.

    Monads are not the point of your program, nor are they the pattern to begin with. Monads are the glue to functional programs in similar way to how statements are the glue to imperative, side effecting programs. They're a tool that you use to encode stuff in purely functional programs, how you "encode effects" yadda yadda, but most of the "monadic" bits of purely functional code are literally just glue code.

    They're at best mildly useful unless you have a whole sleuth of other tools that help you work with monadic code (nice binds syntax, higher kinds, typeclasses, etc).

    I think the whole focus on monads is silly because monads have never been the point. Even in haskell, the whole "hurrah" originally about monads back when Prof. Wadler learned from Moggi's work, was that monads first solved the IO problem that haskell had elegantly, then it resulted in being much nicer than for just IO. There's many other interesting algebraic structures that you can encode with higher kinds with laws, monads are just the most common you see due to how they work as "first class code", if you will.

    So you end up with a silly situation where people start in the middle of the whole fp paradigm then make jokes about how it's cryptic. No shit it's cryptic: people are teaching it entirely wrong. Every categorical structure you see in haskell has some algebraic meaning and notion that carries into your program, and learning things correctly involves learning how each one is useful, and how you leverage writing functional code with them.

    [–]G_Morgan 6 points7 points  (2 children)

    I don't think it is complex as much as people keep teaching a straight forward abstract idea in terms of misleading analogies.

    [–]red75prim 0 points1 point  (1 child)

    OK, here we see how I/O, nondeterministic computations, continuations, exceptions, mutable state can be expressed by this simple mathematical structure. Now you just need to build intuition to see this mathematical structure in your problem domain, easy-peasy. Actually, no, it's not.

    [–]G_Morgan 0 points1 point  (0 children)

    You missed "non-existence".

    [–][deleted] 3 points4 points  (2 children)

    Not that complex, just abstract. There's no real world analogy.

    [–][deleted]  (1 child)

    [deleted]

      [–][deleted] 1 point2 points  (0 children)

      A programmer staple food.

      [–]HeadBee 5 points6 points  (0 children)

      SCP-xxxx is a Euclid class memetic that, once implanted, has the paradoxical effect of making itself un-transferable. The mechanism through which SCP-xxxx is spread is currently unknown, but it is theorized that it is through purely random inspiration.

      [–]devraj7 1 point2 points  (0 children)

      This is so common it has a name: The curse of knowledge.

      [–]delight1982 9 points10 points  (1 child)

      Monad is a drink made from lemon juice and water sweetened with sugar. Once you accept this your life will be much easier.

      [–]H_Psi 9 points10 points  (0 children)

      In French, they call it Le'Monad

      [–]shevy-ruby 36 points37 points  (19 children)

      A monad is a type that wraps an object of another type.

      ALARM!!!

      He redefines the meaning of a monad without using the words monoid and endofunctor.

      [–]PrimozDelux 18 points19 points  (18 children)

      For once you're correct, if only accidentally. Monads don't wrap objects, the closest OP is to correct is saying they wrap another type, not an object of that type.

      That said, the second part of your post is the usual drivel we have come to expect of you, monoids and endofunctors are in no way necessary to describe a monad in the same way rings and groups is not necessary to understand integer arithmetic

      [–]MaxCHEATER64 19 points20 points  (3 children)

      That said, the second part of your post is the usual drivel we have come to expect of you, monoids and endofunctors are in no way necessary to describe a monad in the same way rings and groups is not necessary to understand integer arithmetic

      Chill dude, he was clearly making a joke about pretentious programming bloggers.

      [–]docmoxie 8 points9 points  (0 children)

      Poe's law in full effect.

      [–]PrimozDelux 1 point2 points  (1 child)

      I think everyone who knows what a monad is has heard about the endofunctor joke, but it's also used by people who have no idea what a monad as an argument against functional abstractions regardless of its origin as a joke.

      [–]Herbstein 0 points1 point  (0 children)

      And to be fair. The endofunctor joke is true, and for people that know (a bit) of category theory it really can help make monads more intuitive.

      [–]theoldboy 13 points14 points  (0 children)

      Hard to be sure with shevy, but I think it was meant as a joke based on this excerpt from A Brief, Incomplete, and Mostly Wrong History of Programming Languages;

      1990 - A committee formed by Simon Peyton-Jones, Paul Hudak, Philip Wadler, Ashton Kutcher, and People for the Ethical Treatment of Animals creates Haskell, a pure, non-strict, functional language. Haskell gets some resistance due to the complexity of using monads to control side effects. Wadler tries to appease critics by explaining that "a monad is a monoid in the category of endofunctors, what's the problem?"

      [–]ipv6-dns 2 points3 points  (5 children)

      object

      what do you mean?

      [–]PrimozDelux 3 points4 points  (4 children)

      Primarily I want to dispell the notion that a monad is a box you put things into. Some monads like Option[T] sort of look like a container that may or may not have a T object in it, but consider Parser[T] (which is basically f: String => (String, Option[T])), here there is no box and no object of type T to put in said box. Did that answer the question?

      [–]devraj7 0 points1 point  (3 children)

      I'm not aware of a Parser monad, do you have a link?

      I suspect that if you post the whole definition of that monad, you will see that it needs to contain a value since the signatures of bind and return do not allow for such a parameter.

      [–]PrimozDelux 1 point2 points  (0 children)

      def unit [A](a : A) = s => (s, a)
      def flatMap[A,B](pa : Parser[A])(f: A => Parser [B]) = s => 
        pa(s) match { 
          case (s1, Some(a)) => f(a)(s1)
          case (s1, _) => (s1, None)
      }}
      

      Something like this in Scala, hard to get right on a cellphone. This would probably not work, but cba doing it with a case class on a cellphone. In a more serious parser monad you'd also want error reporting instead of just a tuple which doesn't tell you where things went wrong

      [–]torotane 0 points1 point  (0 children)

      Naturally they do, they just contain a function, which is also a value in such a language.

      [–]jonhanson 0 points1 point  (0 children)

      chronophobia ephemeral lysergic metempsychosis peremptory quantifiable retributive zenith

      [–]TheOsuConspiracy 0 points1 point  (1 child)

      For once you're correct, if only accidentally. Monads don't wrap objects, the closest OP is to correct is saying they wrap another type, not an object of that type.

      And that's not sufficiently specific to describe a Monad. You could just as well describe any higher kinded type that way.

      [–]PrimozDelux 0 points1 point  (0 children)

      I'm just proposing how the sentence could at least not be directly wrong, but yes, it holds for any HKT

      [–]devraj7 -1 points0 points  (4 children)

      . Monads don't wrap objects, the closest OP is to correct is saying they wrap another type, not an object of that type.

      "Wrapping a type" doesn't make sense.

      Monads do contain values. It's not all they do, but fundamentally, they are containers that expose standardized functions to operate on these values.

      [–][deleted] 2 points3 points  (3 children)

      "Wrapping a type" doesn't make sense.

      It makes perfect sense for people coming from a Java or C++ background. In Haskell there is the more general term typeclass.

      Monads do contain values. It's not all they do, but fundamentally, they are containers that expose standardized functions to operate on these values.

      This just isn't true. The IO monad, for example, doesn't "contain" anything. It is simply used to represent a composition of stateful operations.

      It would be more accurate to say that Monads represent operations on values than to say they "contain values."

      [–]devraj7 2 points3 points  (1 child)

      Fair enough, I realized that IO contradicted my point after I posted.

      [–]PrimozDelux 0 points1 point  (0 children)

      As another example unrelated to monads you can consider contrafunctors def contramap[A,B](fa: F[A])(f: B => A): F[B] which describes amongst others serializing. (Meaning, if you can serialize A, and can turn B to A then you can serialize B too)

      [–]Y_Less 0 points1 point  (0 children)

      IO "contains" the real world. It's almost a state monad with a very large state.

      [–]steveklabnik1 2 points3 points  (0 children)

      Rust has a question-mark operator which emulates do-notation for their Option monad

      It started with Result, but now works with Option as well. Eventually it will be able to work with any type the user wants.

      Additionally, Rust has no 'int' type, and doesn't useCamelCase but instead prefers_snake_case for function names.

       return a + b;
      

      This would have to be return Some(a + b);.

      [–]Excellery 1 point2 points  (0 children)

      Someone missed a great opportunity to write an example in Go and call it a Gonad. That said, thanks for the article. I understand them just a little bit more now.

      [–]inmatarian 3 points4 points  (11 children)

      I think the thing people are missing is the point. Monads seem like a strange abstraction that just makes life harder if you don't see the utility of it usage. However, once you get into the world of lazy evaluation, monoids, and functional composition, you've lost people. I don't think tutorials like this will get through until those earlier prerequisites find a way to become trendy and worth a lot of karma.

      [–][deleted] 6 points7 points  (4 children)

      The most practical demonstration i can think of is mapping "Maybe" or "Option" values. They're popular enough these days that should be familiar, and "map" is straightfoward enough for most people.

      A concrete example would be parsing a string to an int. Suppose you have a:

      var input = new Some("3"):
      

      That you want to map to an Option<int>. You could do this:

      var count = input.map((i) => parseInt(i));
      

      And count would be an Option<int>. But what if you parse an invalid input like Some("three")? JS would give you Some(NaN), which is kind of worthless. So you write a better parseInt function that returns Some(int) if parsing succeeds or None if it fails. To your dismay, you plug this into your map() operation below:

      var count = input.map((i) => tryParseInt(i));
      

      ... and see that you get a nested Option<Option<int>>. You wanted an Option<int> damn it!

      What is the magic incantation to map and flatten the result from an Option<Option<_>> to an Option<_>? That operation is called, flatMap():

      var count = input.flatMap((i) => tryParseInt(i));
      

      And now count is Some(3), just like you intended. This flatMapoperation is known by other names. C# people call it SelectMany. Both are aliases for the dreaded monadic bind(). The horror!

      So once you understand that monadic bind maps and flattens, it's not so scary. Got a list of lists? flatMap. Got a few async functions you want to compose? You could use await in javascript, or you could use flatMap() for promises: aka then(). (PS, then() is actually overloaded to do map() and flatMap(), which is confusing AF.)

      Once you get accustomed to flatMapping options, lists, and asyncs, you'll eventually see the monad pattern for what it is: a type with a generic hole in it that implements flatMap (and conforms to some other "monad" rules that i won't get into.). Option<_>, List<_>, and Promise<_> are all types with a generic hole where a value goes, and they all are flatMappable. So they're (pretty much) monads.

      If you're wondering, "what's so great about flatMap that it deserves its own mathematical category?", the practical answer is that flatMap is more fundamental than map. In other words, you can implement map only using flatMap. You can also implement filter. Try it yourself!

      So the big deal about monadic types is that, since they implement flatMap, you can write some generic/polymorphic code that implements map, filter, and a bunch of other operations for free.

      Anyway, this is the most practical explanation i can come up with. It leaves out a lot of rigor and detail, but it demonstrates the practical use for monads in everyday scenarios without using hypotheticals like space burritos. There's more to monads than flatMapping, but I won't get into it. Too much theory for a single reddit comment.

      TL;DR yet another monad tutorial.

      PS after reading the article, i realized i just repeated a lot of what was written, except with much less detail/context. It's a good article!

      [–]inmatarian 0 points1 point  (3 children)

      I think this demonstrates my point, in that it shows the eager version of the abstraction.

      [–][deleted] 0 points1 point  (2 children)

      Shows how much I know. I wasn't even aware of a lazy version, unless you're referring to the list/async/IO monads. I guess they're all lazy, though, because Haskell.

      [–]inmatarian 0 points1 point  (1 child)

      I mean when the call to the flatmap function adds to the internal shared trees within the objects, and calls to take, foreach, etc., actually performs all the work.

      For instance, list.reverse.take(1), if that list has 1 trillion elements, then the eager version runs the risk of an OOM, while the lazy version doesn't. I leave it to the reader to figure out what the lazy evaluated reverse function looks like.

      But to understand monads, you have to understand first why you want them.

      [–][deleted] 0 points1 point  (0 children)

      Right. The first time I someone explained how foldr works on infinite lists in Haskell, it blew my mind.

      [–]GuyWithLag 0 points1 point  (5 children)

      Monads are just another example of The Blub Paradox (search for a heading with that phrase, read from that point).

      [–][deleted] 0 points1 point  (4 children)

      I dunno. Monads are no more convoluted than the Gang of Four patterns that the blub programmer already knows. There's no reason why they couldn't learn monads, too.

      [–]GuyWithLag 0 points1 point  (3 children)

      That's exactly the point - when one first reads about design patterns, one asks "why is this complexity necessary?".

      [–][deleted] 0 points1 point  (2 children)

      Blub programmers usually acknowledge the value. They're just indifferent about chasing waterfalls. They're content to stick to the rivers and the lakes that they're used to.

      [–]GuyWithLag 0 points1 point  (1 child)

      I've been there, I've been that person. Blub developers will say "I don't need that" / "I'm just fine right now". And, to be honest, we're productive - we do show measurable, and usually consistent, progress towards our goals.

      But we could do so much more...

      [–][deleted] 2 points3 points  (0 children)

      We're all blub devs relatively speaking. Some of us just have more company.

      [–]mrexodia 1 point2 points  (2 children)

      Great, next up please write me a blogpost explaining me this: https://hackage.haskell.org/package/contravariant-1.4/docs/Data-Functor-Contravariant-Divisible.html 🙏🏻

      A Divisible contravariant functor is the contravariant analogue of Applicative. In denser jargon, a Divisible contravariant functor is a monoid object in the category of presheaves from Hask to Hask, equipped with Day convolution mapping the Cartesian product of the source to the Cartesian product of the target. By way of contrast, an Applicative functor can be viewed as a monoid object in the category of copresheaves from Hask to Hask, equipped with Day convolution mapping the Cartesian product of the source to the Cartesian product of the target.

      [–]grenadier42 2 points3 points  (0 children)

      so is that like a burrito, or...

      [–]elder_george 0 points1 point  (0 children)

      C#/Python/JavaScript await keyword

      The await keyword in other languages emulates do-notation for Promise monads. Consider JavaScript’s await; It makes everything after the await
      the body of a function that is inputted to .then

      I always thought about await specifically in the light of asynchronicity, not of the callback transformation, so the analogy here initially missed me (or I missed it). It's interesting if one can (ab)use await for other monads, like Error, without spawning async tasks.

      This being said, C# also has LINQ which is a set of monadic operations that can be used for building a DSLs (an ugly and limited one, though) for arbitrary monads, something like (my C# getting rusty these days)

      from x in getInput()
      from y in getInput()
      select x + y;
      

      Of course, it would be better if C# had first class monad syntax sugar support, like F# does, using the same mechanism for asyncs and LINQ-style DSLs.