I will never let monads be in a Clojure project
Sounds a bit aggressive, doesn’t it? Well, let me explain. Monads are fine while you’re playing with them in Haskell. But don’t bring them to Clojure. They are completely foreign beings to Clojure ecosystem. Besides the technical issues, there is a problem with sharing monads across the team. Read the following, and I hope you’ll get into what I’m worried about.
Nobody uses monads in libraries
So, why you’d better not to ship monads into Clojure code? First, they are used
quite rarely in production. Let me open my recent project which is just another
two-in-one boring stuff. An HTTP server plus a single page application. I’ve got
plenty of dependencies here in my project.clj
file. There are about 40 of them
just to cover basic stuff. Database, oAuth, UI… I don’t want to dump the whole
list here. Check this gist for the reference.
The full tree of dependencies expands into multiple screens. Their children libraries, and their children, and so forth. There are about 100 packages in total.
Now guess how many of them use monads? You know that, it’s zero. None of the libraries needs monads to bear their functionality. I’d like to stress that. Some of them provide a really tough stuff: database access, template system, HTTP communication, etc. Yet they work without monadic operations. Why? Just because Clojure is powerful enough. It brings everything you need for convenient development. The authors of those libraries didn’t use monads not because they are not aware of them but because they just don’t need them.
I don’t believe you are experiencing a trouble which is much more serious then something mentioned above and thus craves for monads. There is definitely a plain way for doing this.
Railway Programming is a toy
Moving on, most of the people who are using monads told me, they are fascinated with the idea of Railway Programming. I believe you are aware of this site and their mental model in general. Several years ago I fell into it completely thinking I’ve found the ultimate solution. It didn’t work, I regret to say.
The problem with it is, it’s just a mock. A toy model which looks nice on the screen. At first glance, it reminds a holy grail that you’ve been looking for years and got it finally. So simple: just cut your code into a set of same looking chunks and then build a pipeline.
I’ve been praying for that picture for months.
Suddenly, it becomes clumsy and tough once you try to fit it into a real project. You’ve got to write more code. As I mentioned, none of the libraries really use it. All the functions should be wrapped to return monadic values rather than plain ones.
The standard let
, cond
, when
, and dozens of useful core functions don’t
work with monadic values. You’ll lose a huge stuff for no reason. Instead,
you’ll have to implement monadic versions of let
, if
, etc. Then test them,
write documentation. Why? It’s clear to me, the more code you’ve brought on
board, the less the project is maintainable.
We are already there
The Railway Programming proposes a happy path where each step leads either to the next happy step or to the negative outcome. This is fine, but we’ve already got the same stuff, indeed. These are exceptions. They act the same way.
Take a look at the standard Railway example. They query the database, send email, write a file, etc. Surely, each of these steps is dangerous, whoever argues. But I don’t see a reason for why not to wrap the whole code with a simple try-catch statement? It acts exactly the same. For example, when an error occurs on the third step which is sending email, the entire pipeline terminates so we end up in the catch clause.
Here, we’ve got an exception instance with most of the data we need. What to do next is up to us. We can log the error and fail silently. Or rise another exception with a high-detailed message plus the source exception attached for a cause. That’s is sufficient for maintaining the business logic.
The main reason people turn to monads is, they’ve got lack of knowledge of using exceptions. There is no common rule for handling exceptions and thus it might be tricky sometimes. Some of them must be caught right here, others should flow up. For example, when iterating through the list, I wouldn’t like to stop the whole program once failing on a certain item. I just log the detailed message and go on. Or if I found a user doesn’t have enough permission for that endpoint, I throw an exception so it called by the top-level middleware. It does know how to turn in into a proper HTTP response.
I know this is just an extended version of the GOTO operator. It looks completely imperative but I don’t care. This is a business problem, not a Haskell tutorial.
Monads are built upon strict types
In fact, I don’t see any reason for using monads with a language of dynamic
typing system. Don’t see at all. In Clojure, we’ve got nice collections for
storing either data or a nil
value. Since nil
is treated as an empty
collection and we mostly process collections in Clojure, there is no reason to
worry about whether it was a map or just nil
.
When I doubt if a value I’ve got might be nil
, I just wrap the top-level form
with (when value ...)
which prevents me from dealing with nil
. In addition,
it returns nil
and thus is compatible with threading macros as well.
The very need for monads comes when you’ve got an honest typing system. An honest system is such a one that doesn’t allow to pass empty values (nil, NULL, None, etc) for an arbitrary object like one does in Java. When you are out of this trick, you’ve got to compose a complex type with a bunch of special operations upon it.
A good example might a language with a strict typing system and algebraic types,
say Scala. They haven’t got a Nil
type at all. You cannot just pass an dummy
value instead of a DateTime
parameter. Instead, there is an Option<T>
type
which is a composition of either an instance of <T>
class or a None
value. This is only because of their type system’s limitations.
But this is not our case! In Clojure, we are not suffering from such
things. It’s much easier to check the value for its completeness with the
standard if
form rather than wrapping it into a monad. You overcomplicate
things, and that’s the problem.
By the way, talking about complexity. In Clojure, we don’t like complex things,
we’d rather take simple ones. The entire language is about simplicity, you
know. You either have got some data represented with a map or nil
. That is
simple. That is something you can always check within REPL out from your
editor. Instead, a monad is a wrapper what obscures the original value and
forces me to recover it with special macro. That’s too complex.
Leave other languages alone
That situation with monads in Clojure reminds me something from Python. Since it
has map
and reduce
functions as well as flexible syntax, one believes it’s
possible to program with Python in a functional way. The Internet is full of
tutorials about higher order functions, functors and even monads. My shame, I
used to write my own “functional” library for Python with all that stuff. It was
an mess stiched from chunks of Scala and Clojure. I was really proud of it being
sure this is really a great stuff. So do other authors, I suppose.
One minor detail is, nobody uses those “functional” libraries. I’ve seen a lot
of Python code before switching to Clojure. Let me assure you, a functional way
in Python is a myth. There is no a chance to deliver a Django project in
functional terms. Put map
and reduce
here and there, it won’t affect the
whole picture. After all, the very language is completely imperative so you end
up with changing objects through their lifetime. This is not because Python
sucks, no. I love it. It’s just a matter of different design.
Clojure also has got other design. It doesn’t know anything about monads. Talking in a more broad way, the idea of fetching foreign ideas from other languages doesn’t work. If a language is entirely imperative, functional way won’t apply here. When it has got dynamic types, the idea of adding strict type checks manually neither applies. Otherwise, you get something that we call Chimera. A strange mix of foreign elements that do not match each other. This is subject to break.
What would you do with the team?
One other note is, think for a while about not yourself but the whole team. Look, not everyone is as smart as you. We know you’ve been drilling Haskell by nights but know what? Nobody cares. You’ve been hired to solve business problems, not to tickle your arrogance.
With monads, we’ve got to teach the team first. Some of my friends are really great developers yet they know nothing about monads. I don’t see any reason to violate their brains. For what? To force them to know monads? They are already capable of solving any possible business problem. Live them alone.
I’m not against one-man-band use cases when you are the only one person responsible for everything. Use monads, monoids, do what you want. Yet it’s still an amateur way. You won’t go far as a single developer.
Wrapping up
Using monads is fine when you’ve got a language which is monad-friendly. They should be a part of the language’s design. Haskell or Scala would be a great choice for that.
Railway programming looks fine right until you put that gizmo into a real project. Dull exceptions would be enough.
Don’t borrow too much from other languages. What has been in Haskell should be left behind. It’s Clojure time! Shifting your mind between paradigms is really fun. But not sticking around the only “truthful” way!
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter
Val Waeselynck, 29th Nov 2018, link
I would argue projects like Manifold (https://github.com/ztellman... and Promesa (https://funcool.github.io/p... totally have their place in the Clojure ecosystem (I can tell from experience that not all but many projects would be better off using that instead of / in addition to core.async). These projects expose monadic abstractions, for problems than raw Exceptions could not have addressed.
You Are Here, 29th Nov 2018, link
Completely agree about monads, but rop can be done without them using exception instances as failure values. I've created small lib using that approach:
https://github.com/dawcs/flow
anonymous, 3rd Dec 2018, link
>Monads are fine
until you play with them in Haskell.
until -> while?
Alexis Rodriguez, 5th Aug 2019, link
You can make DSLs in Lisps and verify at macro-expansion time that a lot of static properties hold just like with Monads, but more flexible but yeah there is no mechanism to tell you this and this function is pure and stuff.
Ivan Grishaev, 5th Aug 2019, link , parent
fixed, thank you
Ivan Grishaev, 5th Aug 2019, link , parent
Well, the only library that I'm OK with as Manifold what brings chaining macros. That really works, although Manifold is about async stuff, not monads. I can stand with a single library but adding more monadic stuff would be over me.
Pierre Thierry, 17th Sep 2019, link
I don't get it. You seem to say that we already have Railway Programming with exceptions and they are great and people don't know them enough, but you close with saying nobody should put Railway Programming in a real project.
Do you mean that nobody should use exceptions in production?
Ed, 18th Oct 2021, link
"The main reason people turn to monads is, they’ve got lack of knowledge of using exceptions"
Most people who use monads are people who have used exceptions and don't like them, and the reason people don't like exceptions is because as you said "I know this is just an extended version of the GOTO operator". yes, and GOTO sucks for reasons that you probably know, but then you say "It looks completely imperative but I don’t care. This is a business problem, not a Haskell tutorial." What does that even mean? So you're OK with with unclear code and execution paths?
The main problem people have with exceptions is that it makes your code unpredictable.
Sure, it's easy to wrap everything in a try/catch and do the error handling at the bottom. You probably save some time, but that will lead to a codebase where the norm isn't to handle errors on the spot, but to simple defer their handling for later.
Programmers like exceptions because it allows them to segregate their error-handling code from their happy-path code, but the problem is that error handling code shouldn't be treated as "exceptions". Errors happen all the time and good software should handle as many of them as possible. Exceptions train programmers into thinking that error-handling is some kind of afterthought, but things like monads force programmers to handle errors on the spot.
Another example is golang. It doesn't have exceptions. Although it doesn't force programmers to do this, the convention is to return an error as the last return value and force users to write
if err != nil
every time because that's how good software is written.You Are Here, 15th Feb 2022, link
nice reading especially in context of new Pact library)
rebcabin, 12th Oct 2022, link
in clojure.test.check.generators, tgen/let is essential. It’s monadic bind.
Rick C, 10th Feb 2023, link
Do you use lists with map? filter? reduce? You use Monads Do you use optionals? streams? pipes? transducers? You use Monads Do you use promises? futures? deferred? You use Monads
The monadic pattern is everywhere, because it is useful.
With all due respect I don’t think you entirely know what you are talking about. Clojure uses monads everywhere!! You are confusing the concept with some implementation you dislike.
It’s amazing how this naive and ill informed articled keeps popping up every time someone dislikes a Monad library or similar, and then cites it as a “valid source”
You need to get your concepts straight.
Haskeller, 24th Jul 2023, link
People just don’t get what monads are, and they cargo cult them and bring the monadic interface into dynamically-typed languages where a macro would probably do a better job.
Concept #1: Functor
A functor lifts something into a new category (set, if you’d like), while preserving structure of the arrows (functions) of that something.
That is to say, in Haskell, functors lift a value/type into a new value/type, and provide the accessor function fmap that lets you map a function into the new type.
The mathematical requirements for a Haskell functor to be a functor simply come down to:
must be able to lift onto a type fmap must exist compose of fmaps over functions is equal to fmapping the compose of the functions fmapping the identity function is the same as directly applying the identity function
i.e, in math terms, it respects the composition of functions within a category, and in computer science terms, the compiler can do some nice optimizations provided the functor laws hold.
To be a monad:
Monads add two further operations, join / mu / flatten, which compresses two functor layers of the same functor together, and pure / eta / new, which injects a value into a neutral functor layer such that when join is applied to it, it’s as though it were never there in the first place.
Join / mu / flatten is also associative; if you have a multiply nested functor and join multiple times, it doesn’t matter which two functorial layers get flattened first, as long as you don’t swap the order of nesting.
If this sounds useless or trivial, well, flatmapping (bind, chain, composition of fmap and join/mu/flatten) monads is a concept of sequencing (produce new monadic context, merge the contexts), and can be used to provide a mathematical description of tons of imperative or imperative-like actions.
The deal, though, is that, in languages with tons of monadic types (Haskell, in Haskell, State, Environment, Write-Only State, List, Array, Optional, Exception, etc… Scala), including monadic types for exceptions and so on, it’s convenient to have a proper concept of monad and provide a first-class monadic interface.
You can also have tons of monadic types (Rust), but have distinct interfaces for each monadic type, and that has its benefits.
But unless you’re using a ton of monadic types in your language, you don’t need a monadic interface.
Clojure doesn’t use monadic types; it sticks to straight exceptions much of the time (Haskell also has exceptions, but the problem with its exceptions is that only the impure / IO layer can process the exception thrown). Hell, it’s dynamically typed with a glue-on gradual typing type-checker, so it doesn’t make sense for Clojure to go with monadic types.
Many Haskellers themselves aren’t really interested in monads these days because monads as effect providers tend to have difficulty composing, and when they do compose via monad transformer stacks, there’s usually a substantial performance penalty.
So why stuff a questionable feature of Haskell into your language? It’s just cargo-culting.