It's a common attitude that functional languages with immutable collection semantics, such as Clojure, are for a) pretentious language geeks, or b) actual genius programmers. I'm in no position to defend against point a) given the body of my writing on this blog, so today I'd like to write an article about point b).
I'm not such a clever guy. I struggle a lot with mundane programming tasks, every day, just like most programmers. The reason I enjoy working with Clojure so much has nothing to do with how much I can accomplish applying a purpose-built genius language to my rockstar ninja talent. It's because Clojure lets me save brain cycles by limiting the complexity of the programs I write and the libraries I work with.
Now Java, there's a language for geniuses.
One of the biggest problems with large software written in object-oriented languages is that, to know the instantaneous value of a mutable object attached as a field to another mutable object attached to … and so forth, you have to be able to trace, in your head, the lifecycle of this object. This is a task fit for a genius!
It's difficult to explain why this is a problem with small examples, as
I usually do, because small examples are tautologically easy to fit in your
working memory at once. Sure, ok, the
Person object has an
age field, and when
updateAge function is called the age field increments according to the
birthDate field and the current time. That's a pretty questionable design
for an aging
Person object, but it's not so complicated that you can't
keep it straight. The problem is much more pronounced when you're working
with large systems, often ones you don't understand or even have access to
the code for.
The other day, I was debugging an objective-c project, and had a problem where the delegate value of a view controller was changing from the object that I put there on initialization, to a string derived from I-don't-know-where later on in a different method. I don't want to have to babysit my code like that, especially when following the stack trace takes me into assembly land. (Assembly code is like Albania. I went there once in college, and it was interesting and educational, but I don't expect to return.)
In a language that allows mutability-by-default, I have to keep tabs on not only the code path that leads to the calling of any function/method, but also all the spots where some other code gets passed a reference to it. Every single reference that exists to my object is a way that someone can reach in and dick with it, and that makes for some very challenging bugs sometimes.
Think less, do more.
Clojure code isn't like this1. It has my back on this entire class of common defects. When I get a value in a function that isn't what I expect, all I have to do is trace that value right back up the call stack to wherever it came from. This is an idiot-friendly design.
So that's why immutable is good. What about functional? In my mind, Clojure's functional paradigm exists mostly as a supplement to the immutable one. Immutable collections don't help you as much as they could be if you can re-assign existing variables with new collections.
The other nice thing about functional programming is that, once you get used to it, pure functions are a very simple unit to build software out of. There are very few programs that can't be broken down into a black box which receives input and produces output, and this process can be recursively applied until your program is just a series of small functions that call other small functions.
Finally, REPL access further reduces the necessity to model things by providing direct access to individual bits of your code. No need to model the whole thing in your head when you can just get right in there and poke at it.
Clojure still has some rough edges, and it's different enough to make new users coming from OO and imperative languages uncomfortable. All I ask is that you don't confuse that subjective discomfort for objective difficulty. Clojure will challenge your notions of software design, but it will save your brain.
: Of course, Clojure allows mutability in the form of the STM constructs
agent, and also through
transient and lately
volatile. But you
really have to go out of your way to use these – they're not by any means the default.
They just exist for those situations when state simplifies an algorithm, and
tend to exist in independent snippets small enough to fit in your brain at once.