Hacker News new | ask | show | jobs
by ashtonkem 2199 days ago
I fully grok monads. I’ve done enough reading and usage of them to understand them.

They are not a useful abstraction for programming. They’re mathematically correct, but that’s not the same as what an industrial engineer needs.

The big warning sign is monad transformers. Alone, monads are totally fine, but the issue is that you rarely want one. So you end up with this unwieldy tower of transformers that would make an enterprise Java engineer worried.

5 comments

As an industrial engineer, monads are the best solution I've ever found to the cross-cutting concern problem. I need to be able to put custom cross-cutting concerns on my functions - things like must-happen-in-database-transaction, record-in-audit-log, must-have-this-authorisation-level, record-these-statistics. If your language doesn't have monads, you end up using reflective proxies, metaclasses, decorators, bytecode modification, macros, or something equally incomprehensible.

I'd love to have a better alternative. Maybe one day one of these "effect systems" will actually get a production-quality implementation. But until then monads are the least-bad option out of everything that I've seen tried.

Absolutely. It's worth noting that every single effect system implementation that currently exists also leverages monads at the user level, even if they're doing exciting type-theoretic things underneath.
I firmly believe that monads (and monad transformers) are exactly what an industrial engineer needs, for the following important reason.

A monad describes its scope in such a way that writing code outside of its scope is a compile time error. If your code needs certain capabilities, it must invoke the computational context of those capabilities (which is usually a monad in Haskell). If it doesn't, then the context can (and should) be omitted.

It's worth noting that monad transformers are themselves monads. So drawing a distinction between "monads" and "monad transformers" to say that one is good and one bad is not very meaningful. The composability of monads, as exemplified in MTL, Transformers and similar, is a positive sign of their power and not a red flag.

If your code ends up with an "unwieldy tower" of transformers then that's a strong indication that the principle of separation of concerns has not been adequately followed. The fact that Haskell makes that evident seems to me like a benefit.

I’ve been thinking recently about how monads in functional programming are analogous in some sense to inheritance in class-based object oriented programming. Each abstracts a useful pattern and lets the programmer write a certain type of code quite elegantly. Each is also profoundly limited by being based on a single type(class), i.e., the monadic structure or the base class/interface.

If you want to use multiple instances of the same pattern in the same place, you have to start making design choices about how to relate them. For example, you might nest monad transformers in a certain order. You might define a class hierarchy.

Sometimes the correct design will be dictated by the context. However, sometimes it won’t, and then you’ll have to make what is essentially an arbitrary decision at the time, yet one that may be expensive to change or adapt to if it turns out to be inconvenient later.

And finally, as long as everything stays nice and “linear” nothing too bad seems to happen, but in realistic programs you might end up needing to combine multiple monadic effects or wanting your class to inherit from multiple base hierarchies. That is often when awkward edge cases start creeping into the design. Suddenly, instead of the elegant patterns from the glossy brochure, you start seeing ugly and brittle warts like explicit casts to resolve ambiguities in which virtual method should be called or manual lifting through multiple layers of monadic effects.

In each case, the response has been to move away from the original patterns towards something more flexible where the compromises are not as deep. In OOP, composition tends to be favoured over inheritance these days. In languages like Haskell, there is a lot of interest in effect systems that could offer a less rigid way to combine monadic effects than monad transformer stacks.

Even if you have separated concerns by having method 1 return Monad transformer A and method 2 return transformer B, you will still have to combine the results of both your methods at some point.
Sure. But if you're doing it right, that combination happens in a place whose sole responsibility is doing that combination. The only case where you have to carefully interleave A and B is if your business logic really does involve carefully interleaving two disparate effects, and in that case the complexity is genuinely there in the domain and it's good to surface it explicitly (or else you're modelling it wrong).
It might be nice to be able to compose monads some other way than transformers, but I don't know what that would be. Transformers are unwieldy, yes, but they're also completely trustworthy. If you try to assemble them randomly, without thinking about it, yes, you will have a bad time, in much the same way that you will have a bad time if you try building any abstraction without thinking about it.

Yes, they can be a useful abstraction (just like anything else); I have the same sort of feeling about your response as I have about "checked exceptions are evil".

This isn't true at all, promises in JavaScript are monadic, as are chains of conditional logic. Stuff like this is literally everywhere:

if a: b = f(a) if b: c = g(b) ... else: p()

Which as a monad would be something like

A >>= f >>= g >>= h >>= ...

I've built several classes with a covert "return" and "bind" operation to keep this kind of programming clean, I just don't tell anyone it's a monad.

>They are not a useful abstraction for programming. They’re mathematically correct, but that’s not the same as what an industrial engineer needs.

Pray tell then, how do you sequentially compose functions operating on values wrapped in an ADT like Maybe or Either?

You don't. You just do it the dumb old simple way, with some boilerplate here, and some boilerplate there. It may not be elegant, but it's not a blocker. Haskell needs more than this, if it's to find wider adoption.
Experience shows that it is a blocker. Programmers will resort to virtually anything to avoid that kind of boilerplate - early return, macros, exceptions as a language feature, reflective proxies.
One monad at a time is fine, but the problem starts when you need multiple. Monad transformers are completely unergonomic.