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.
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
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
When I doubt if a value I’ve got might be
nil, I just wrap the top-level form
(when value ...) which prevents me from dealing with
nil. In addition,
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
which is a composition of either an instance of
<T> class or a
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
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
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
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.
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