Thursday, December 26, 2024

I Don’t Use Monads

I consider myself a “mostly functional” programmer. I think about programs in terms of data flow and transformations and mappings from input to output. I avoid side effects and mutable state where it isn’t necessary. But I’m not a purist. I’m happy to setf an instance slot when it makes sense.

Monads are the darling of the functional programming community. To hear it, they are the key to understanding the universe. They provide a way to encapsulate side effects and mutable state in a pure functional way.

But I don’t often use them.

There are a few reasons for this. I don’t work in Haskell, where monads are a ubiquitous part of the language. If you use Haskell, you’re going to use monads. But what about the languags that I do use? Monads of course “work” in any language that supports higher-order functions, but they are not always idiomatic or the obvious way to accomplish a task.

The primary use of monads is to mimic the look and feel of an imperative language, with its sequential execution, side effects and mutable state, in a language that is purely functional. Monads are a way to implement progn and setf in Haskell. But Lisp already has progn and setf.

And an imperative programming style is largely inferior to a functional programming style. I prefer to not think imperatively and write imperative code. I've seen many Haskell programs that used monads just so they could use do notation and write code that looked like C. That seems like a step backwards.

Monads are complicated. They essentially come down to functional composition of curried functions hidden behind some syntactic sugar. So long as you don’t have to look under the hood, they are reasonably easy to use. But when you do have to look under the hood, you’ll find a maze of twisty, turny, higher-order procedures. In Lisp, a well placed setf can cut through this functional Gordian knot.

2 comments:

Antti Holvikari said...

I agree that doing imperative programming with monads is sometimes like going backwards. But at the same time having worked with non-pure languages (like TypeScript) I often wish I was using monads.

I'm assuming when you speak about Monads you actually mean doing I/O with Monads, because Monads are really useful for many data types like Either and Option. But setting that aside I think there are real benefits of Monads that make me wish I could use them more often:

- It's super nice to treat side-effects as first-class values. You can use higher-order effects that take other effects as input like a `retry` combinator.
- Monads are an abstraction, not a concrete implementation like `progn` or `setf`, which means that you can decide what "sequencing" means in your context.
- Last but not least, your imperative code becomes referentially transparent, which is already a huge deal IMO.

Here's a somewhat related post I thought you might like http://conal.net/blog/posts/can-functional-programming-be-liberated-from-the-von-neumann-paradigm

Mark Safronov said...

Well, we cannot do I/O without being imperative, right? I/O is a side effect, and the purely functional style cannot model side effects.

(ofc theoretically we can model side effects in the pure functional style by having an "environment" object which returns being changed after an I/O operation but it's not realistic to implement)

So until we switch away from von Neumann's machines the "functional core, imperative shell" is our unchangeable destiny, no need to be overly sad about it IMO.