[HN Gopher] Inventing Monads ___________________________________________________________________ Inventing Monads Author : stopachka Score : 66 points Date : 2020-08-29 18:22 UTC (4 hours ago) (HTM) web link (stopa.io) (TXT) w3m dump (stopa.io) | centimeter wrote: | I think the hard part for most people is moving beyond | Maybe/Either. It's hard to find motivating examples when most | languages lack denotational semantics and everything implicitly | happens inside IO all the time. | Scandiravian wrote: | That is something I've struggled with as well. Do you have any | suggestions for resources, that could help overcome that | challenge? | centimeter wrote: | Nothing off the top of my head. The way I really got monads | was just writing Haskell code. It's one of the very few | programming languages with a clear denotational semantics for | everything (as a result of being pure), and you end up with | lots of simple problems like "how do I keep this value around | and 'mutate' it" that are conveniently addressed by a monad | like the State monad. | ryanjshaw wrote: | If you come from a C# background, this book is excellent: | https://www.manning.com/books/functional-programming-in-c- | sh... | | It does have some minor shortcomings in my opinion (e.g. if | you haven't already started on the path of reinventing monads | yourself you may struggle to immediately understand why this | is a big deal and how it will save your life, the author | should have used LanguageExt instead of their own library as | LanguageExt is actively maintained and extremely well thought | out, the book stops just short of becoming practical in the | sense of "here's how to start a new C# project while thinking | in functions", etc.). | | You might also consider the LanguageExt guide itself: | https://github.com/louthy/language-ext/wiki/Thinking- | Functio... | Scandiravian wrote: | I actually found the LangExt package a few months ago, when | I started a new job and had to use C# as the default | language (I've primarily been using Python, Typescript, and | Rust in my previous jobs) | | I spent some time explaining the advantages of an FP | approach to my colleagues and LangExt has started to pop up | in their PRs, which I'm very happy about | | It's definitely a great library and I'm really impressed | with the effort that's being put into it! | | I'll give the book you're suggesting a read. I think it | might help cover some of the gaps in my knowledge, which is | exactly what I'm looking for, so thank you for the | suggestion | fizixer wrote: | So do you need monads in Haskell because that's the only way you | could do some of the things that are trivial in an imperative | language (albeit verbose and arguably inelegant)? | Koshkin wrote: | Yes. (For example, ML, being impure, does not need this.) | mrkeen wrote: | No | exdsq wrote: | This is the paper that introduced them to FP, if you're | interested in the original reasoning. | | https://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/b... | ape4 wrote: | That code that "is getting pretty ugly" seemed ok to me. | tarkin2 wrote: | [Edited away] | touchngthevodka wrote: | As someone who isn't familiar with functional programming, what | benefit does this give us over throwing an error when trying to | access a resource that doesn't exist? | ianhorn wrote: | A monad's special function application lets you write much | simpler code in certain situations. | | Say you're working with some data structure that contains/emits | numbers: a pointer to a resource containing a number. A list of | numbers. A function that returns a number. An optional number | (or null). | | A common operation is unpacking that structure to get a number, | applying a function to the number, and packing it back up: | Reading from the pointer, applying the function, and returning | a pointer of the result. Applying the function to each element | of the list and returning a list of the results. Composing a | function with another function. Applying a function to the | optional number or just returning the null. | | When you're writing code on this, it's error prone to do the | unpacking, application, and repacking. It's much simpler if you | can write code that looks like `def f(x): return exp(x)/x + | 23`. Much more testable too. If you have two or more of these | structured things, it might get even more error prone. It's | much easier to write code that takes three integers and does | stuff, instead of writing code that takes three | pointers/lists/functions/optionals. | | Monads are part of a hierarchy that abstracts that. Anything | that defines that sort of function application in a | particularly convenient way is a monad. There's more to it, but | that's why it's useful. | | It lets you write code dealing with the things in your data | structure, letting you mostly ignore the structure itself. | | ------- | | In this specific situation, say you want to replace your error | handling with something else. Maybe it writes to a log file | then errors. Or maybe it does something fancier. Or you change | the way you get the resource as well as the erroring to | something fancy. As you swap out the "structure" code, with a | monad it's just switching to a new monad, rather than | refactoring the business logic related code. It's a nice | separation of concerns. | raiflip wrote: | What if you need to access 5 resources in a row, any of which | can throw exceptions? A monad centralizes the repeated logic in | its flatMap function. | edflsafoiewq wrote: | I think the comparison is to something like this | try: user = getUser() profile = | getProfile(user) pic = getProfilePicture(profile) | thumb = getThumbnail(pic) return thumb | except Missing: return None | raiflip wrote: | That example seems a bit odd as a design. You're throwing | an exception to represent a no result, but suppressing it | to convert the exception to a none. Monads give you the | convenience while being consistent. | Koshkin wrote: | Well, the problem here is that using exceptions to | implement the program's logic is considered bad practice. | TheMatten wrote: | Well, nothing stops you from implementing equivalent wrapper | using error mechanics (that could actually make it faster in | some cases), but turning this idea into first-class value | allows you to abstract over it easily. E.g. in Haskell, base | library comes with lots of functions for manipulating monadic | wrappers in generic ways, mapping, sequencing and threading | through them in common way (once you start using them, you | actually realise that a lot of business logic that looks | perfectly reasonable in common languages ends up being | boilerplate that can be avoided easily using simple | combinator). | | Few of them are actually bound to syntactic sugar known as "do- | notation", that let's you write that sequenced code in post as | if you were binding simple variables, adding branching, | effectful statements or auxiliary definitions along the way. | This really pays out when you start turning simple monads into | so-called "monad transformers", that let you stack multiple | behaviours/wrappers on top of each other, keeping the same | pretty do-notation untouched. | jlg23 wrote: | WRT this specific post: Graceful error propagation with zero | boilerplate code for passing through errors. If all you need to | know is "nope" at the end of some computation, you won't have | to handle all different "nopes" you encounter en route. Or the | other way round: You can write lots of code without checking | input values (for null, in this case) because you have a | guarantee it won't be called if the input value is invalid | (null here). | | EDIT: I have serious problems with the post, because a) it | claims that discovering one application of monads is | understanding monads, b) for me the true strength of monads | shines in strictly typed languages, everything else is just an | approximation of the concept. | marcosdumay wrote: | One is just plain code that you write on a library. The other | is a special construction on the compiler that will solve this | specific use case. Your question is on the wrong way around. | You should be asking why do you need specialized compiler | support for just that use case. | | Notice that that short introductory article already has | examples of two different monads. People use many more of them. | Scandiravian wrote: | Throwing an error is implicitly making a decision, that there's | no way to recover to a working state for the program | | A lot of times that's a perfectly fine decision, but six months | down the road when the code has grown a lot, you might find an | alternative way to get the resource on a fail | | If you made a decision to throw an exception immediately after | failing to get the resource, you then have to either rewrite | the logic, which can be very expensive, or catch the error, | which bloats the code (throwing the exception is now redundant | and is fixed by adding code that catches that exception) | | By instead putting the return value in an appropriate monad, | you can postpone throwing the exception until you're sure that | there's no way to recover | | Throwing an exception is still something that's necessary | occasionally, but it should not be done until there's no | possible way to recover, and be done in a way, so it's easy to | rewrite if a way to recover becomes available at some future | point in time | lmm wrote: | You need a special language feature to "throw an error", which | can break your reasoning about code. A seemingly harmless | refactor like swapping two lines might completely change your | behaviour because one of those lines actually threw an error. | It becomes very difficult to do things like manage a resource | properly (ensuring it's always released), to the point that you | probably end up adding more special language features to handle | that. | raiflip wrote: | I really like how this article builds up from a simple real life | use case. | | Shameless plug for my own article that did something similar | about a week ago: https://medium.com/@ameltzer91/an-easy-to- | understand-monad-g... | stopachka wrote: | Thank you for the kind words, and nice article! : ) | jjjbokma wrote: | Nice article, I linked to it from my tumblelog | https://plurrrr.com/archive/2020/08/19.html 10 days ago. Thanks | for writing this. | k__ wrote: | I think, the main benefit of FP is alsoits main problem. | | Patterns like the monad are so abstract, that ob its own nobody | knows what to do with it, but it makes them so very powerful. | | OOP patterns, on the other hand, come from a more inductive | source. They are more concrete, but also not as powerful. Easier | to grasp, but less concise. | | We need a step (or more) between the definition of FP concepts | and their application, to make all this more approachable for the | average programmer. | cjfd wrote: | In various places on the web one can find the quote that | 'Dependency injection is a 25-dollar term for a 5-cent concept'. | | I feel it is the same with monads including all the false | suggestions that one might need to understand category theory and | similar such nonsense. | chongli wrote: | _suggestions that one might need to understand category theory | and similar such nonsense_ | | It's definitely unnecessary to understand category theory to | work with monads. What is helpful, however, is having some | comfort with math. | | What does that mean exactly? It's a level of comfort in working | with definitions, properties, operations, special elements, | proofs. People can get so frustrated because they think they | don't understand _what a monad is_. Like they want to hold it | in their hand the way they would an apple or a tennis ball. | | When you're comfortable with math you kind of lose that need to | think about an object concretely. You start to only care about | the definitions, properties, axioms, laws, theorems, etc that | concern a particular object. Then you just play around with a | few examples and see the implications of these things. That's | all there is to it. The power comes from the abstraction. It | can take time to become comfortable with abstract concepts | though. | Koshkin wrote: | Another thing that is often missed is that we think we | "understand" something when in fact we just got used to it. | Even such simple concept as "number" would probably be very | difficult to _explain_ to someone (who either doesn't know | what a number is or wishes to "really understand" it). | edflsafoiewq wrote: | If only DI had a clear definition like "monad" does, I might be | able to understand what it is. | goto11 wrote: | It just means passing a dependency as an argument to method | or constructor. Seriously, it is just that! | agumonkey wrote: | I used to restrict DI as dynamic binding but someone told | me that for business, having a special category of | arguments to be tweaked as see fit is useful. It's variable | at the system level maybe ? | [deleted] | Twisol wrote: | Which, in FP, is all but invisible: the arguments to a | function literally are its dependencies. | | I think OOP has more primitive concepts (and more mutation) | than FP, so dependency injection in OOP also includes | object construction and often mocking effectful operations. | That's why it gets its own name in OOP, while being more of | an ambient idea in FP. | stopachka wrote: | Imagine a function, that accepts another function as an | argument. | | Badabing badaboom, the essence of dependency injection | scubbo wrote: | Isn't that Inversion Of Control, not Dependency Injection? | I've always thought of Dependency Injection as about | "declaring a class in terms of its dependencies' | interfaces, but allowing a framework to take responsibility | for instantiating the actual dependency objects" - whereas | "a function that accepts a function" is IoC (e.g. | https://kentcdodds.com/blog/inversion-of-control/) | | They're very closely related concepts - both abstractly | saying "let me define unit of logic in terms of how it | composes passed-in units of logic", but the distinction | between "objects that need to be instantiated and injected | at construction time" and "functions that are passed in at | runtime" is pretty large. | stopachka wrote: | I think the distinction may blurry a bit, if you consider | classes as a fancy way of writing higher order functions. | | i.e: | | You can think of a class as a higher order function, that | takes in a list of arguments (constructor), and returns a | list of functions, that are defined within the closure of | those arguments | | -- | | Hence, to me, the essence of these things are the same -- | thought you are right that when people talk about | dependency injection, they are also implying a specific | way that these dependencies are provided (by the | framework, usually magically) | chowells wrote: | There's also the classic http://blog.sigfpe.com/2006/08/you- | could-have-invented-monad... | | Written by a 3-time Oscar winner, no less! | guerrilla wrote: | > Writen by 3-time Oscar winner, no less! | | Sorry, what? Their About page is blank. | dllthomas wrote: | The author is Dan Piponi: https://pwlconf.org/2018/dan- | piponi/ | | https://www.imdb.com/name/nm0685004/ | dang wrote: | Looks like there has been only one small discussion from 2009: | https://news.ycombinator.com/item?id=958789 | ducaale wrote: | To understand monads, I think it helps to know a language where | expressing such a concept is more natural. Although I do not | claim to know 100% what monads are yet, coming across these | features made it easier for me to understand the concept a little | bit. | | - Algebraic data types | | - OCaml's let expressions | | - F#'s computation expressions | | I also think that it is important to write something that | naturally requires the use of monads. One thing that spring to | mind is Parser Combinators. | | - https://fsharpforfunandprofit.com/posts/understanding-parser... | | - https://www.youtube.com/watch?v=N9RUqGYuGfw | ridiculous_fish wrote: | > This begins to get us to the fundamental abstraction of a | monad: a box, with an interface for map, and flatMap | | Before I understood monads, I read variations of the above | sentence a million times, always got stuck here: | https://i.imgur.com/McThkuh.png | | How does this abstraction let us perform IO, do in-place | destructive updates, etc? The answer is that it _doesn 't_. These | must be primitives supplied by the runtime. A monad like IO has | its two functions (return+bind), _and a bunch of other magic | functions_. It 's obvious now but that fact is hardly ever | stated. | | I wonder if anyone has tried explaining from the other direction? | Rather than building up to Maybe, try building down from a desire | to print Hello World. | chowells wrote: | I think most of this happens because lots of people say things | like "the IO monad" when they actually just mean "IO". It | misleads people into thinking the monad part is important. It | isn't. IO is the tough concept. Monad is almost trivial. | ridiculous_fish wrote: | Good point, I agree. But you must learn it to get anything | done. Grappling with IO + monad + do-notation all at once is | hard! | | I once spent a day thinking I had misunderstood something | fundamental, but really I just had an errant tab instead of a | space. | chowells wrote: | Ok, yes. Monads as a concept are almost trivial in | comparison to thinking in terms of the IO type. But that | doesn't mean they are easy and don't add more hurdles to | get over. I think I undersold how daunting it can be to | face all the new things at once, and you're right about it. | yomly wrote: | As a recent Haskell learner, I found the indentation part | of Haskell hard to learn and a little undiscoverable/hard | to google. | | The number of times I've used curly braces, single lines or | expression substitution to bail me out is many... | dllthomas wrote: | + type inference + return-type polymorphism! | goto11 wrote: | Yeah this is like saying objects lets us perform IO in Java, | and then going deep into explanations of what objects are and | what rules they obey (Liskov substitution principle etc) - | instead of just explaining that you call println() to write | "hello world". | yomly wrote: | We often forget how much assumed knowledge (and accepted | knowledge) went into learning OOP. | | Turing Machine and Lambda Calculus are two entry points into | computational thinking, is it really so alarming that if | you've only learned one that trying to learn the other will | make you feel like a beginner all over again? | raiflip wrote: | A monad really is just a wrapper around a value, with flatMap | and return. | | For some kinds of monads, like maybe, that's all you need. | | But, some other kinds of monads need more logic. List is one, | IO is another. | | In OOP language, sometimes all you need is the parent class. | But sometimes you need to extend the parent and add more. | ratww wrote: | It's actually more like an abstract class! Even the Maybe | monad has some implementation, it's just super simple :) | | https://hackage.haskell.org/package/base-4.14.0.0/docs/src/G. | .. | anchpop wrote: | What you say is correct, but let me try and explain a little | further. The Haskell function `getChar` is used to get a | character from stdin. It has the type `IO Char` and is a | totally, 100% pure. In fact, it isn't even a function, it's | just a value. | | You can write `getChar` as many times as you want, and you will | get the same result every time, and it will have nothing to do | with whatever character is being sent to stdin. Instead, what | you will get is an instruction, saying "please get a character | from stdin". | | What the IO monad allows you to do is to compose instructions | together. For example, you could write `getChar >> getChar". | This makes a new instruction that says "please get a character | from stdin, discard it, then get a character from stdin". The | `>>` operator means "follow the instruction on the left, | discard the result, then do the instruction on the right". | | Your _entire_ program is made by composing instructions like | this, into one new huge instruction that specifies your entire | program 's behaviour. You assign that to the special name | `main`. At runtime, the instruction is executed. | | The only bit of compiler magic that's necessary is the code for | interpreting the instructions and actually executing them. In | theory, although haskell doesn't let you do this, you could | write `getChar` yourself, and just return the same thing that | `getChar` does, and it would behave identically. | | This I feel is the simplest explanation for why monads are | necessary in Haskell. They allow you to conveniently specify | what you want your program to do, using only pure functions and | values. Why do you want all values to be pure? Many don't | because it's kind of a pain, but Haskell kind of exists to see | if a lazy, pure functional language can be fun to use and many | find that it is. Hope this helps someone! | choeger wrote: | > In fact, it isn't even a function, it's just a value. | | What's the difference? | dllthomas wrote: | Functions are values, but values that are not functions are | those that cannot be supplied an argument. | ridiculous_fish wrote: | Great description of IO, but I think it doesn't actually | motivate monads. Your example of `getChar >> getChar` could | be simply `[getChar, getChar]`. Why can't we just compose | instructions into an ordinary list? | | The answer is that we can, and Haskell used to work this way | [1]! So as 'chowells' observed the monad-part of IO is almost | incidental. | | 1: https://stackoverflow.com/questions/17002119/haskell-pre- | mon... | thatcherc wrote: | Ah, thanks - this is a really nice explanation. From reading | a few "what is a monad" posts in the past, I get how the a | monad can chain functions in sequence for imperative-style | execution, but how the information like 'getChar' actually | got in was never clear. It all comes down to the runtime I | guess. | | I'm favoriting this comment so I can come back the next time | I forget how all this works :) | agumonkey wrote: | to me the issue is twofold: | | - cultural reliance on lambdas as the universal force (nothing | against lambdas, but outside the fp world, there will be too | much culture shock) | | - representation as computation, the monad is only kinda | building a graph to be evaluated, and each node can be wrapped | with rules as you see fit | AnimalMuppet wrote: | Hmm. Your last line makes me think that they're, essentially, | using monads to represent S-expressions. That seems... | somewhat clumsier than just using S-expressions. You gain a | really good type system, though. ___________________________________________________________________ (page generated 2020-08-29 23:00 UTC)