12 ms·
As far as monad tutorials go, this one seems quite good. I like the categorization of monads between "containers" and "recipes". However, I personally think th
by brooke2k 1y ago
As far as monad tutorials go, this one seems quite good. I like the categorization of monads between "containers" and "recipes".
However, I personally think that monad tutorials tend to give people the wrong impression and leave them more confused than they were before, because they focus on the wrong thing.
A monad is not a complex concept, at all. IMO a more useful way to present the topic would be with one separate lesson for every common monad instance. Start with Maybe, then IO, then maybe State and List, and so on... because ultimately, every instance of a Monad works very differently. That's why the pattern is so useful in the first place, because it applies to so many places. (Note: this is a criticism of monad tutorials in general, not this one in particular, which seems to do a decent job on this front).
In my experience, people new to Haskell focus way too much on getting the "a-ha" moment for monads in general, when really you want a bunch of separate "a-ha" moments as you realize how each instance of a monad takes advantage of the pattern differently.
I also tend to think that monads are best demonstrated in Haskell rather than in other languages, if only because the notation is so much less clunky. That may just be me though. (EDIT: well, also because almost no other languages have typeclasses, so you have to approximate it with interfaces/traits/etc)
Also FYI: in part 2, the code examples have extra newlines in between every line, which makes it hard to read (I'm on firefox, if that matters).
- pdhborges 1y agoIf all monad instances work differently what is the value of the Monad interface? What kind of usefull generic code can one write against the Monad interface. Related: https://buttondown.com/j2kun/archive/weak-and-strong-algebraic-structures/ https://buttondown.com/j2kun/archive/weak-and-strong-algebra...
- ChadNauseam 1y agoLots of useful generic code. MapM is a version of `map` that works with any Monad, `sequence` works with any monad, and so on. These are used very frequently. But the bigger benefit is when syntax sugar like `do` notation comes in. Because it works for any Monad, people can write their own Monads and take advantage of the syntax sugar. That leads to an explosion of creativity unavailable to languages who "lock down" their syntax sugar to just what the language designers intended. In other words, what requires a change to other languages can often be a library in Haskell.
- bjourne 1y agoWhat can a Haskell monad do that a Python class cannot? 99% of all monads I've seen only facilitate local state manipulation.
- jerf 1y agoIt can do it type-safely. Monad is a weird type that a lot of languages can't properly represent in their type system. However, if you do what dynamically-typed scripting languages do, you can do any fancy thing that Haskell does, because it is weakly typed in this sense. (The sense in which Python is "strongly typed" is a different one.) What you can't do is not do the things that Haskell blocks you from doing because it's type-unsafe, like, making sure that calling "bind" on a list returns a list and not a QT Window or an integer or something.
- Tainnor 1y ago> Monad is a weird type that a lot of languages can't properly represent in their type system. While true, a lot of FP-inspired libraries in the majority of languages that don't have HKT will just implement one or several specific monads as well as the common operations on them. This creates some redundancy and slight inconsistency, but often the shared vocabulary is still strong enough to carry around expectations more or less, even if it's not explicitly enforced by the type system. That's how you can have sequence(): List<Either<L,R>> -> Either<L, List<R>> in e.g. Kotlin, for example. Even in Scala, where you actually can define a monad typeclass (trait), there are very popular libraries like ZIO that effectively give you a monad without actually adhering to any Monad trait. I believe they do this for type inference reasons.
- AnimalMuppet 1y agoThis may be a dissenting opinion, but... Haskell tried to avoid mutable state. "Local state manipulation" was not really a thing you could do in Haskell, deliberately. Then someone figured out that you could (ab)use a monad to do that. And because that was the only way, whenever they needed to manipulate state, Haskell programmers reached for a monad. So it's not "what can a Haskell monad do that a Python class cannot". It's "what can a Python class do in a straightforward way that Haskell has to use a monad for, because Haskell put the programmer in a straightjacket where they couldn't do it without a monad". It's basically a pattern to get around the limitations of a language (at least when it's used for state).
- moomin 1y agoYour basic problem is that your programming language can’t express the concept cleanly. You need what’s called “Higher-Kinded Types”. To give you a concrete example, in C# Func<A, B>, List<A> -> List<B> Func<A, B>, Task<A> -> Task<B> Func<A, B>, Func<C, A> -> Func<C, B> Can’t be expressed using a generalisation. But in Haskell, you can write (Functor F) => Func<A,B>, F<A> -> F<B> One of the biggest things that makes monads hard to understand is that the type systems of most languages can’t represent them. Annoying, that includes the “typeless” ones.
- hollerith 1y ago>One of the biggest things that makes monads hard to understand is that the type systems of most languages can’t represent them. Annoying, that includes the “typeless” ones. Well, yeah, since a monad is a type, then a "typeless" PL will not be able to represent it.
- WorldMaker 1y agoC# is a fun example because there is ongoing work to support Higher-Kinded Types in it: https://paullouth.com/higher-kinds-in-c-with-language-ext/ https://paullouth.com/higher-kinds-in-c-with-language-ext/
- jameshart 1y agoI'm sorry, I'm not sure I understand entirely what you are trying to express by Func<A, B>, List<A> -> List<B> That said, in C#, you can write: List<A> listA; Task<A> taskA; Func<A, B> func; List<B> listB = from i in listA select func(i); Task<B> taskB = from t in taskA select func(t); And if it can resolve a method on List<T> called 'Select' that takes a Func<T, S> that returns a List<S>, and a method on Task<T> called 'Select' that takes a Func<T, S> that returns a Task<S> this will compile. In other words, I kind of think that Select methods (which can be Select extension methods, of course) amount to functors in C#?
- instig007 1y agonow write a single function that performs from x in xs select fn(x) and define a signature for it where `xs` and `fn` are the only input arguments, so that it accepts both `listB` and `taskB` without a compilation error.
- NickPollard 1y agoTraverse (or foldM) is probably a good start, likely the most useful monad-generic (or applicative-generic) function, that is simple but incredibly powerful and useful. More generally, Monads essentially support function composition between monadic functions, so you can use it to write code that is agnostic to the monad it runs in. This can let you write e.g. prod. Code that is in IO or Async or Maybe, but for unit testing run it in Identity. Also, it allows syntax sugar such as do notation that makes it clear to work with even when you know which monad you're working in.
- jerf 1y agoAs I so often do, I find it helpful to analogize Monad to Iterator for questions like these, because it's a typeclass/interface/etc. that people are more used to and does not have that aura of "if I feel like I understand it I must not understand it" attached to it that blocks so much learning. You extremely often use iterators in a context where there's no way you could usefully slot in just "any" iterator and have some useful code. Suppose you have an iterator that iterates over the links that appear in an HTTP document, and write some code to fetch the HTTP resources so referenced. Well, obviously, "writing against the iterator interface" doesn't do you any good in that case. It's not like you can slot in an iterator that iterates over prime numbers to such code and get anything out of it. What you can do with the Iterator interface is provide extremely generic tools that can be used against any Iterator, like, take the first x, skip every other one, reverse the iterator list (if finite and for a price), filter the results against a type-specific acceptance function, all kinds of things: https://docs.python.org/3/library/itertools.html https://docs.python.org/3/library/itertools.html These tools do not depend on the details of what the iterator is or how it works, only that it is one. In this case you might even use something as powerful as "give me an iterator and a function to run against the value that comes out of the iterator and I will run it in a parallel map and limit the number of workers and handle errors in this specific way", but all that code has no specific knowledge about URLs or fetching things from the web or anything like that. It just knows it has an iterator and a matching function for the value coming out. Similarly, "writing to the Monad interface" gives you access to a wide variety of tools that work across all things that implement the monad interface: https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Monad.html#g:2 https://hackage.haskell.org/package/base-4.21.0.0/docs/Contr... What exactly they do depends on the underlying monad implementation. It happens that they turn out to be very useful in practice a lot of the time. You can also create new compositions of the tools that only pay attention to the interfaces, like, "drop the first x values and then filter the rest" for an iterator, though often the libraries ship with the vast majority of what you need. Written against the interface specifically you can only use exactly what is in the interface. But you also have the concrete types to work with, with whatever it is they do. Just as you can't really do much "real work" against just "something that provides a next value" when you have no idea what that next "value" is, but iterators are very useful with specific types, monads are the same way. (You can then later work up to code that is allows swapping out which monad you may be using depending on how it is called, but I prefer to start here and work up to that.)
- ww520 1y agoHere is an analogy. List is a container whose elements can be any type. There are general operations applying to a list, e.g. map, reduce, filter, find, etc. Any data type (int, float, or bool) of list elements can use these same operations regardless. It’s similar for monad. If you can provide a unit constructor to turn an object value into a monad value and a “map” operation that unwraps a monad value, applies a function to it, and wraps the result, you have monadized the object type. Your objects can participate in any algorithm operates on monads. The monad algorithms are the same. The only things different are the unit constructor and the mapping function.
- maleldil 1y agoYou're describing a functor. For monads, you still need bind or join.
- ww520 1y agoIt's not a functor; "map" is bind. Here's an example in non-Haskell as I was addressing non-Haskell audience. class Maybe { value: int; static wrap(v: int) { return new Maybe { value = v } } // map() applies fn on the unwrapped value. fn returns a Maybe. map(fn: (x: int) => Maybe) { return this.value == null ? Maybe.wrap(null) : fn(this.value); } } Here Maybe.wrap() is "unit" and Maybe.map() is "bind" in Haskell lingo. The whole thing is a monad. You can chain call with it. Maybe.wrap(5) .map(x => Maybe.wrap(x + 1)) .map(x => Maybe.wrap(x * 2))
- maleldil 1y agoHaskell bind is usually spelt flatMap in other languages, like Rust and even Java. map is used for functor fmap.
- chriswarbo 1y ago> a “map” operation that unwraps a monad value, applies a function to it, and wraps the result It can be misleading to think of "unwrapping" a monadic value, since the monad interface does not support it. For example, there's no way to implement a function `List<T> -> T` using monad operations; it requires something entirely separate (e.g. indexing into a List, in this case). What monads do provide is `join`, which turns nested monadic values into flat ones, like `List<List<T>> -> List<T>`. Even this seemingly trivial example is interesting though, since there are many ways to "flatten" a List<List<T>> into a List<T>: we could concatenate (e.g. depth-first), interleave (e.g. breadth-first), diagonalise (to support infinite lists), operate on chunks at a time (e.g. iterative deepening), etc.
- ryandv 1y agoSee for instance the MonadIO typeclass from Haskell [0]. Constraining against this typeclass allows one to write monadic code / do-notation that works with any monad, so long as that monad supports the execution of IO statements. Now for instance, arbitrary effects (error handling, resource management, etc) can be composed on top of an IO monad (e.g. via monad transformers), and MonadIO code, that is written to only depend on the IO effects, can still be executed in these contexts with more effects layered on top. [0] https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Monad-IO-Class.html https://hackage.haskell.org/package/base-4.21.0.0/docs/Contr...
- tel 1y agoThe more constrained your theory is, the fewer models you have of it and also the more structure you can exploit. Monads, I think, offer enough structure in that we can exploit things like monad composition (as fraught as it is), monadic do/for syntax, and abstracting out "traversals" (over data structures most concretely, but also other sorts of traversals) with monadic accumulators. There's at least one other practical advantage as well, that of "chunking". A chess master is more capable of quickly memorizing realistic board states than an amateur (and equally good at memorizing randomized board states). When we have a grasp of relevant, powerful structures underlying our world, we can "chunk" along them to reason more quickly. People familiar with monads often can hand-wave a set of unknowns in a problem by recognizing it to be a monad-shaped problem that can be independently solved later.
- ryandv 1y ago> There's at least one other practical advantage as well, that of "chunking". > When we have a grasp of relevant, powerful structures underlying our world, we can "chunk" along them to reason more quickly. This is one thing I've observed about Haskell vs. other languages: it more readily gives names and abstractions to even the minutest and most trivial patterns in software, so that seemingly novel problems can be quickly pattern matched and catalogued against a structure that has almost certainly been seen before. One example: I want to run two (monadic) computations, and then somehow combine together their results (with some binary operation). Such a trivial and fundamental mode of composition, that seems to lack a name in almost every other programming language. Haskell has a name for this mode of composition, and it's called liftM2. Never again will you have to re-write this pattern for yourself, leaving yourself open to error, now that you have this new concept in your vocabulary. Other languages will happily let you reinvent the wheel for the umpteenth time, or invent idiosyncratic patterns and structures without realizing that they are just particular applications of an already well-studied and well-worn concept.
- maleldil 1y agoNote that this is general enough that you don't need a Monad for this. Applicative is enough (liftA2).
- tome 1y agoEverything in here, for a start: https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Monad.html https://hackage.haskell.org/package/base-4.21.0.0/docs/Contr... https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-Traversable.html https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-...
- teiferer 1y ago[dead]
- lmm 1y ago> If all monad instances work differently what is the value of the Monad interface? What kind of usefull generic code can one write against the Monad interface. Code that composes a bunch of operations, for whatever kind of composition those operations need (some people call Monad "programmable semicolons", because it's a lot like sequencing). E.g. traversals of datastructures, or some kind of "do this in a context" operation. Essentially any function you pass a "callback" to should probably be written in terms of monads so that it can accept callbacks that need different kinds of composition beyond just being called at different points in the control flow.
- StopDisinfo910 1y agoThe truth is that it’s not a very useful abstraction in and of itself. You can build some generic tooling on top of monads and applicatives and that tooling is useful and can give familiarity to new data structures but objectively that’s true mostly because monads are so common in Haskell. Thinking monads are common for this reason is reversing cause and consequence. So why are monads so prevalent in Haskell, you will ask. Because there is sugar to make their use easy. And why is there sugar? Because I/O uses a monadic interface. That was Haskell new idea. You can easily keep track of side effects with the type system if you use a monadic interface and some sugar.
- deleted 1y ago[deleted]
- kqr 1y ago> one separate lesson for every common monad instance. Right on. This is the "What Are Monads" fallacy: https://entropicthoughts.com/the-what-are-monads-fallacy https://entropicthoughts.com/the-what-are-monads-fallacy
- brooke2k 1y agoWow, this is a great post, thank you for sharing. It echoes my thoughts exactly.
- aeonik 1y agoI think you are right. I don't think I've fully mastered the concept yet, but what you are saying resonates with me. I've been trying to grok monads for almost a decade. More and more I'm beginning to realize how "mundane" the concept is, and the usefulness really is just that specific pattern of mundanity. Similar to pipelines on Linux, they are pretty basic, but their ubiquity and their use in composing unrelated things together is really, really useful, and you only get that if you use them in a variety of different ways. Monads are extra cool because of the mathematical rigor behind them, and that's what I'm still trying to appreciate.
- macrolocal 1y agoYep, you need category theory to express something as trivial as the definition of a monad.
- zmgsabst 1y agoThat’s what category theory does well: broad collections of trivialities in a unified definition.
- cynicalkane 1y agoWhat helped me grok the mathematical rigor is: If you have a series of monad operations that exist purely in monad world -- in Haskell, if your expression is parametric over the type of the monad -- you shouldn't have to worry about how you do it. This is what monads being categorically commutative ("a monoid in the category of endofunctors") buys you. You want to turn monad X into monad Y? Sure, just join, flatten, return, bind in whatever way makes the type checker happy. Anything that only uses what's in the Monad typeclass must necessarily be a monad morphism, so if you're generic over your monads, you get that for free. And of course `fmap` and `bind` are required to be parameterized monad morphisms, so there's a lot you can get for free.
- chongli 1y agoI think you mean associative. Neither monads nor monoids are commutative.
- billmcneale 1y ago> That's why the pattern is so useful in the first place How useful, really? Monads don't even universally compose, which is what most people sell the concept for.
- lambdas 1y agoActions compose, types (generally) don’t. So Monad X and Monad Y may not make a valid Monad Z, but Kleisi composition very much exists for actions within a monad.
- billmcneale 1y agoBut the whole promise of monads is precisely that they are a type that can compose. It basically allows you to pipe successive function calls returning different types by lifting these types into a monad. Don't get me wrong, that promise is very powerful and in the rare few cases where it works, it unlocks beautiful composition, but the simple truth is that monads are really not that useful outside of Haskell (and I'd say, it's even questionable within).
- brooke2k 1y agoMonads don't compose between different instances, but monad transformers do.
- Bjartr 1y agoA container monad is just a recipe monad where the recipe for getting the value is "here's the value"
- polygot 1y agoThanks for the feedback! I didn't expect my post to garner a lot of attention. I am totally ok with rewriting part 1, potentially to make it more concise to prevent confusion (wow, this post is super long, monads must be complex!) is what I'd like to avoid. I can reproduce the double line issue in part 2, this was my mistake and I missed it as part of my editing process. I'll delete part 2 while I make the corrections.
- lo_zamoyski 1y agoI think "monad" is overloaded, or at least there are varying depths of understanding that are confused. From a programming perspective, the definition of monads is clear. bind :: m a -> (a -> m b) -> m b return :: a -> m a You can start using monads immediately, and in a language like Haskell, things click fairly quickly, because monads are used everywhere and taken seriously in that language. But the implications and consequences of this definition for monads aren't always obvious, like how they can be used to structure operations or whatever. And then there's the category theoretic business of monads which you don't need to understand for most programming purposes. That might be a source of confusion. As you more or less say, people have vague, built up expectations about monads. They expect something heavy and mysterious and doubt they're understood them according to the first definition. But the basic definition is straightforward. Numbers are like this, too. You understand what a number is (a quantity). You can perform all sorts of operations and calculations using them without knowing number theory or the philosophy of mathematics.
- Avshalom 1y agoI mean at it's most basic "monads" are -a data type with some hidden information -a lot of functions that can ignore that hidden information -some functions that can act (switch|add|mutate|signal|sequence) on that hidden information people seem to think talking about flatMap somehow makes this intuitive despite the tautological issue of flatMap only making sense if you already know what's going on.
- deepsun 1y agoYes, and I don't even see the value in generalizing to one Monad concept. It only makes things worse, as then one is tempted to share terminology between different kinds of monads. E.g. there's no reason Maybe's flatMap is called the same as List's flatMap, it might be more readable to call them differently, as some libraries do.
- PaulHoule 1y agoI came to the conclusion that a List<X> is a good generic data structure, for instance in cases where the cardinality is supposed to be 0..1 it is often less trouble than a nullable scalar or an Optional<X> and you have cases where you’re going to get a list anyway such as if you are querying a relational database. (Often people write awkward code to turn that result into a nullable/Optional and then more awkward code to turn it back to a list later) Lists work almost exactly the same in most languages whereas there is often something weird about null, there might not be Optional, it might have something weird about it, etc. For multi-language distributed processing, particular if JSON is involved it’s worth a try. To be fair I write a lot of Java where Optional is a train wreck in so many ways not least it could be null anyway, you are allocating objects needlessly, and I just see people get hypnotized by awkward code also they write bugs or scan right past them.
- instig007 1y agoyes, exactly, not sure why your comment was downvoted. Also, generally it's not cardinality of 0..1, it is `[]` vs `xs:[]`, that is - either empty, or a multitude of values, where 1 is a specific instance of the multitude (singleton).
- magarnicle 1y agoI found helpful. It really clarified a few things for me.
- monkeyelite 1y agoAlso formal mathematical objects aren’t always like real world objects. What’s a ring? It’s a thing with these properties
- bdangubic 1y agowith this comment you joined a list of seven million devs that have written a monad tutorial :)
- Tainnor 1y ago> In my experience, people new to Haskell focus way too much on getting the "a-ha" moment for monads in general, [...] I feel this is true in general for mathematics (and therefore by languages whose design is heavily inspired by maths). A lot of people not familiar with university-level maths think that they need to understand what some mathematical concept "really means", but modern mathematics is a structural science. It looks at things that have entirely different semantics (symmetries, conservation laws, integers, matrices, Rubik's cubes, ...) and noticing that they all have the same structure (they're all groups) and therefore we can say something about all of them simultaneously. That doesn't mean that intuition is useless. Once you have thoroughly understood what makes a group a group or a vector space a vector space, it's totally normal to e.g. consider a space of functions and think of them in your head as if they were arrows in a Euclidean space (the analogy breaks down at some point, but it can carry you a certain way). That's also why it's fine to think of a monad as a container or as a burrito or whatever once you've actually understood the concept. But you can't really short-circuit this process in my opinion.