Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> I forgot about monads

I've been solving business problems with code for decades. I love pure, composable functions, they make my job easier. So do list comprehensions, and sometimes, map and filter. Currying makes sense.

But for the life of me, no forum post or FP tutorial that I could find explained monads in clear language. I've googled "what is a monad" once a year, only to get the vague idea that you need monads to handle IO.

I wondered if my brain was broken, but now I'm wondering if most FP adherents are simply ineffective communicators: they've got an idea in their head but can't/won't express it in a way that others after them can understand. In other words, the exact same reason why TFAuthor was corrected by his employer.



So, one way to understand a monad is that it's essentially a container type with "map" and "flatten" operations. Let's say your monad is type M<T>. The "map" operation allows you to take a function T->U and a container M<T>, and transform each element, giving you a container M<U>.

"Flatten" takes a container of type M<M<T>> and returns a container of type M<T>. So a List<List<Int>> becomes a List<Int>.

Now comes the trick: combine "map" and "flatten" to get "flatMap". So if you have a M<T> and a function T->M<U>, you use "map" to get an M<M<U>> and "flatten" to get an M<U>.

So why is this useful? Well, it lets you run computations which return all their values wrapped in weird "container" types. For example, if "M" is "Promise", then you can take a Promise<T> and an async function T->Promise<U>, and use flatMap to get a Promise<U>.

M could also be "Result", which gets you Rust-style error handling, or "Optional", which allows you to represent computations that might fail at each step (like in languages that support things like "value?.a?.b?.c"), or a list (which gets you a language where each function returns many different possible results, so basically Python list comprehensions), or a whole bunch of other things.

So: Monads are basically any kind of weird container that supports "flatMap", and they allow you to support a whole family of things that look like "Promise<T>" and async functions, all using the same framework.

Should you need to know this in most production code? Probably not! But if you're trying to implement something fancy that "works a bit like promises, or a bit like Python comprehensions, or maybe a bit like Rust error handling", then "weird containers with flatMap" is a very powerful starting point.

(Actual monads technically need a bit more than just flatMap, including the ability to turn a basic T into a Promise<T>, and a bunch of consistency rules.)


> and they allow you to support a whole family of things that look like "Promise<T>" and async functions, all using the same framework.

You've highlighted here the part that would actually explain the purpose of a monad, but not explained it. You don't need a monad abstraction to have things with monadic properties, and indeed often reality isn't quite perfectly shaped to theory, so forcing your object to fit the abstraction can be costly. One very obvious cost is that you no longer get descriptive names of what the monadic bind does; you have to infer it from what you know of the type.

The one thing a monad abstraction definitely gives you is the ability to write code that's generic over all monads. This is weird because this almost never happens outside of strongly functional languages.

If you'll forgive linking to a decade-old Reddit post, I've talked about this before.

https://reddit.com/r/programming/comments/4t6a6q/functional_...

(also, parent for context: https://reddit.com/r/programming/comments/4t6a6q/comment/d5f...)


> You don't need a monad abstraction to have things with monadic properties, and indeed often reality isn't quite perfectly shaped to theory, so forcing your object to fit the abstraction can be costly.

Please note that I was trying to explain what a monad is, to somebody who wanted to understand. I also suggested that people writing typical code shouldn't actually need to know this in order to do their jobs:

> Should you need to know this in most production code? Probably not! But if you're trying to implement something fancy that "works a bit like promises, or a bit like Python comprehensions, or maybe a bit like Rust error handling", then "weird containers with flatMap" is a very powerful starting point.

Monads are an incredibly stripped-down mathematical structure ("container-like things that support flatMap"). And as such, people who design certain types of programming languages or libraries may benefit from being aware of monads. At least for languages with closures. Surprisingly few languages can actually support monads as a first-class abstraction in the language, because to make first-class monads nice you need a certain kind of type system. Which often isn't worth it.

Where I probably differ from your opinion is that I think implementing "almost monads" like JavaScript promises is very often a mistake. The few places where JavaScript promises break the monad laws are almost all nasty edge cases and obscure traps for the unwary. Similarly, if you implement list comprehensions that break the monad laws, most likely you just get awful list comprehensions.

There are exceptions. Rust has a lot a "almost monads", but this is mostly because Rust function types are a mess thanks to the zoo of Fn, FnOnce and FnMut. Rust would be a simpler and easier-to-learn language if Future<...> actually followed the monad laws. But in this case, it sadly wasn't possible, and I would argue that Rust is worse for having so many "almost monads."

This may all make more sense if you knew my tastes in programming languages, which is "languages where all the parts fit together cleanly with no surprising edge cases that prevent 'obvious' things from working." One way you can accomplish this is to have some kind of simple mathematical structure underlying your language, and to avoid adding dozens of features that almost follow clean rules. C++ never took this approach, and so C++ library designers need to be aware of all sorts of interactions between weird corner cases.

So another way of summarizing my argument is "If you have list comprehensions that somehow don't follow the monad laws, then you're going to confuse users and permanently add technical debt to your language. Make sure it's actually worth it."


The Rust example is illustrative, and indeed I was thinking about it when I wrote my post. One valid angle is to say that languages should hide the reality of the machine from the user any time it would get in the way of the pure semantic description. Another angle though is to say that actually these concerns are important, and if the result is that the idealised abstractions aren't sufficient to capture them, then it's correct to put away the abstraction. It's not like ‘simple and easy to learn’ is a function of this elegance either; Haskell is hard to learn and Go is easy, so clearly there's something else to it.

You also mention list comprehensions forming a monad. I think this is also a good illustration of the difference. In Haskell the structure of a type tells you the dependency tree of a computation, so it's not a problem that Monad maps the type to itself. Your list monad is just a bunch of thunks pointing to each other either way. In imperative languages, types describe what has been reified and how it's organised in memory. In these, a list monad is an actively bad abstraction; you almost always want to distinguish the stateful computational pipeline (eg. an iterator) from the source and target storage (eg. an array). Neither of those are monadic for good reason.


I think this might be the clearest explanation I've seen. Nice work!


How is IO a container type? What does it contain? Thinking of a monad as a container makes sense for some types like List and Maybe, but not all.


https://tech.nextroll.com/blog/dev/2022/11/11/exploring-mona...

The thing that always makes FP concepts click for me is seeing them explained in a language that isn't Haskell or similar.

I don't know why people are so obsessed with using Haskell for tutorials. Its syntax is a nightmare for newcomers and you can do FP in so many other languages.


For instance, C#:

  public static System.Collections.Generic.IEnumerable<TResult> SelectMany<TSource,TCollection,TResult>(this System.Collections.Generic.IEnumerable<TSource> source, Func<TSource,System.Collections.Generic.IEnumerable<TCollection>> collectionSelector, Func<TSource,TCollection,TResult> resultSelector);
compared to:

  (>>=) :: m a -> (a -> m b) -> m b
Guess which one I googled, and which one I typed from memory.


In your c# sample full namespaces are unnecessary and will add only noise in this context.


Haskell have some syntactic sugar which makes monads nice to use, which is why monads are popular in Haskell.

Explaining monads in JavaScript or C# might show the mechanics but will not show why anyone would actually want to use them, since it just result in overly convoluted code.


I strongly disagree. Monads are used all the time in non-FP languages. Parser combinators are one common example. It's just a programming pattern which gives the benefits of global variables without the downsides. They work perfectly fine without dedicated syntax.

I was very confused about what they were until I saw an article similar to the one I linked, and then I realized that I had actually been using monads all along, I just didn't know they were called that. I think a lot of developers are in the same boat.


I dispute that monads are used “all the time” in non-FP languages. Can you provide some examples?


The association between IO and monads is just that Haskell uses a monad for IO as a clever hack to support IO in an otherwise pure and lazy language. But this is just one use of monads. Monads in themselves does not have anything in particular to do with IO.

Monads are a pattern for function composition. In most languages it is verbose compared to alternatives (like map and filter), but Haskell have some syntactic sugar which makes it a nice succint way of chaining functions.

The problem with monad tutorials is they try to explain monads as some abstract, universal concept disconnected from practical code. But try to explain functions or classes the same way and it would also be incomprehensible for someone unfamiliar with the concept.

It is not due to ill will, it is just that Hakell-fans tend to have a background in mathematics and therefore default to the most abstract explanation possible, where software developers prefer to start with something practically useful and then generalize from there.

You probably wouldnt understand ‘map’ either, if it was explained to you by a haskelite, since they tend to explain functions in terms of their type signatures rather than what they actually does.

Monads should be exlained through practical code examples in Haskell since this is the context where it makes sense.


> But for the life of me, no forum post or FP tutorial that I could find explained monads in clear language. I've googled "what is a monad" once a year, only to get the vague idea that you need monads to handle IO.

Another phrase to search for is "railway oriented programming"[0], which technically describes a monad called a Kleisli[1]. Still, there are many introductions in a lot of languages based on the term above.

HTH

EDIT:

Something to keep in mind is that the term "monad" identifies a mathematical concept having well-defined behaviors, not a specific construct. So searching for "what is a monad" is akin to searching for "what is a happiness."

0 - https://medium.com/geekculture/better-architecture-with-rail...

1 - https://bartoszmilewski.com/2014/12/23/kleisli-categories/


You've mentioned list comprehensions, map, and filter, so I suppose you mostly used these concepts with lists/arrays.

One question you could ask yourself is, how could you reproduce list comprehensions without special syntax ?

Another way to view monads is by following the types. With map, you can chain pure functions from one type to another to be applied on lists (hence the (in -> out) -> ([in] -> [out]) ) . How would you do that chaining with function from one type to another but wrapped in a list ( (in -> [out]) -> ([in] -> [out]) ) ?

Then you can think about how it could be applied to other types than lists, for example, nullable/option types, result types, async/promise types, and more hairy types that implement state, reading from an environment, etc...



I felt similar with lenses. The problem lens solve is horrible. You don’t even want that problem.

FP can be the pragmatic as well. You’re going to glue up monad transformers, use lenses like there’s no runtime cost, and compute whatever you need in days but at least you know it works. Maybe there’s accidentally quadratic behavior in lifting or lenses but that’s by design. The goal is to just throw software at things as fast as possible as correctly as possible.


> I felt similar with lenses. The problem lens solve is horrible. You don’t even want that problem.

Lenses abstract properties in a composable manner. How is this problem horrible?

> FP can be the pragmatic as well. You’re going to glue up monad transformers, use lenses like there’s no runtime cost, and compute whatever you need in days but at least you know it works. Maybe there’s accidentally quadratic behavior in lifting or lenses but that’s by design. The goal is to just throw software at things as fast as possible as correctly as possible.

Any abstraction can be used inappropriately. Slavish adherence to an approach in spite of empirical evidence is a statement about those making decisions, not the approach itself.

In other words:

  A poor craftsman blames his tools.


Lenses: solve a problem elegantly (if you can hide the boilerplate) but inefficiently. A self caused problem by having extremely nested records. How did you get to a point where you have a structure that's hard to play with?

Lenses are exactly glue for throwing software at things as fast as possible as correctly as possible. A poor tool.

The very need for lenses often indicates that the data model has been designed in a way that's hostile to direct, ergonomic manipulation. A glued up steampunk contraption, a side-effect of throwing software at everything as fast as possible. Invented in a language environment where they can't be efficient.

Monad transformers: lift has quadratic complexity and runtime cost. Not really composable. Similar to effect systems in other languages, control flow becomes very unclear, depending on the order of application.

Lenses and monad transformers are just a nice trick that you shouldn't ever learn.

But I agree with your last statement, many of these libraries are just poor craftsmans giving us new tools that they made for problems we never want to have.

It's similar to dependency injection, why would anyone need a topological sort over dependencies and an automatic construction of these dependencies? Is it so hard to invoke functions in the right sequence? Sounds like you've made a program with too many functions and too many arguments. (or in oop, too many classes with too much nesting and too many constructor args)

These tools are "pragmatic". Given the mess that will naturally arise due to poor craftsmanship, you'll have these nice tools to swim well in an ocean overflowing with your own poop.


> Lenses: solve a problem elegantly (if you can hide the boilerplate) but inefficiently. A self caused problem by having extremely nested records. How did you get to a point where you have a structure that's hard to play with?

I see the value of lenses from a different perspective, in that they can generalize algorithms by abstracting property position within an AST such that manipulation does not require ad hoc polymorphism. For example, if there exists an algorithm which calculates the subtotal of a collection of product line items, lenses can be used to enable its use with both a "wish list" and a "purchase order."

Another thing they cleanly solve is properly representing a property value change with copy-on-write types. This can get really ugly without lenses in some languages.

I respect your take on them though and agree their definitions can be cumbersome if having to be done manually.


I understand your examples, but I'd say ASTs are rare and they're already a convenience, not a performance or efficiency choice. You're using ASTs because you'll be able to write other code quickly.

For the wishlist and purchase order, just think of the boilerplate you have to write to get 1 computation for variable data shapes, compared to doing what you want 2 times.

Copy-on-write types are easy if they are shallow, I'd question why they're so deep that you need inefficient lens composition to modify a deep value. We've already invented relational structures to deal with this. I'm assuming you care about history so copy-on-write is important and is not purely an exercise in wasteful immutability.


> For the wishlist and purchase order, just think of the boilerplate you have to write to get 1 computation for variable data shapes, compared to doing what you want 2 times.

This example is simple enough to not have to use lenses for sure. Another example which may better exemplify appropriate lens usage is having properties within REST endpoint payloads used to enforce system-specific security concerns. Things like verifying an `AccountId` is allowed to perform the operation or that domain entities under consideration belong to the requestor.

> Copy-on-write types are easy if they are shallow, I'd question why they're so deep that you need inefficient lens composition to modify a deep value. We've already invented relational structures to deal with this. I'm assuming you care about history so copy-on-write is important and is not purely an exercise in wasteful immutability.

While being able to track historical changes can be quite valuable, using immutable types in a multi-threaded system eliminates having to synchronize mutations (thus eliminating the possibility of deadlocks) and the potential of race conditions. This greatly simplifies implementation logic (plus verification of same) while also increasing system performance.

The implication of using immutable types which must be able to reflect change over time is most easily solved with copy-on-write semantics. Lenses provide a generalization of this functionality in a composable manner. They also enable propagation of nested property mutations in these situations such that the result of a desired property change is a new root immutable instance containing same. Add to this the ability to generalize common functionality as described above and robust logic can be achieved with minimal duplication.

It is for these and other reasons I often find making solutions with immutable types and lenses very useful.


As a Scala/FP newbie, i was stuck in this rut for years then i just stopped caring. it actually doesnt matter to me much. ZIO helped due to the eschewing of mathematical terms (compared to things like Cats ). In other words, you don't really need to know to get things done. Monads are incredibly intuitive once you start composing them.


Monads are just monoids in the category of endofunctors.

/s

Monads are, in my head, just a wrapper around a type. Or a box another type is inside. For example we can have an Int and we can put the Int in a box like Maybe<Int>.

Imagine Python code that gets a value from a function that is an Int or None, then another function that takes an Int and returns an Int or None, then another function that takes an Int and returns an Int or None. How hellish is that to handle? If not None, if not none, if not none, ad nauseam...

In Haskell I could use a Traverse function that takes a monad and passes the Int value to the next function or handles the None error, avoiding all the boilerplate.

Other Monads are like the State monad - a box that contains some variables I want to maintain over functions.

Or an IO monad to handle network calls / file calls.

It's probably not a perfect analogy, but I work with functional languages and it tends to hold up for my beginner/intermediate level.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: